diff --git a/package-lock.json b/package-lock.json index 60989d11..2f594c24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6784,18 +6784,15 @@ } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "color-convert": "^1.9.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=4" } }, "node_modules/any-base": { @@ -7544,31 +7541,26 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=4" } }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=0.8.0" } }, "node_modules/change-case": { @@ -7807,21 +7799,18 @@ } }, "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "color-name": "1.1.3" } }, "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, "node_modules/color-support": { @@ -7885,18 +7874,6 @@ "node": ">=8.0.0" } }, - "node_modules/command-line-usage/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/command-line-usage/node_modules/array-back": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", @@ -7906,65 +7883,6 @@ "node": ">=8" } }, - "node_modules/command-line-usage/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/command-line-usage/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/command-line-usage/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/command-line-usage/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/command-line-usage/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/command-line-usage/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/command-line-usage/node_modules/typical": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", @@ -10048,12 +9966,12 @@ } }, "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/has-property-descriptors": { @@ -10987,6 +10905,76 @@ "node": ">=10" } }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jimp": { "version": "0.22.12", "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.22.12.tgz", @@ -12641,6 +12629,18 @@ "typescript": "^5.x" } }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -14257,6 +14257,76 @@ "node": ">=10" } }, + "node_modules/replace-in-file/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/replace-in-file/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/replace-in-file/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/replace-in-file/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/replace-in-file/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/replace-in-file/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -15633,15 +15703,15 @@ } }, "node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "engines": { - "node": ">=12" + "dependencies": { + "has-flag": "^3.0.0" }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "engines": { + "node": ">=4" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -17089,6 +17159,37 @@ } } }, + "node_modules/vite-plugin-checker/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/vite-plugin-checker/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/vite-plugin-checker/node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -17113,6 +17214,24 @@ "fsevents": "~2.3.2" } }, + "node_modules/vite-plugin-checker/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/vite-plugin-checker/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/vite-plugin-checker/node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -17122,6 +17241,15 @@ "node": ">= 12" } }, + "node_modules/vite-plugin-checker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/vite-plugin-checker/node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -17146,6 +17274,18 @@ "node": ">=8.10.0" } }, + "node_modules/vite-plugin-checker/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/vite-plugin-inspect": { "version": "0.8.7", "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.7.tgz", @@ -18361,6 +18501,72 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/prisma/game-v2.js b/prisma/game-v2.js new file mode 100644 index 00000000..14562865 --- /dev/null +++ b/prisma/game-v2.js @@ -0,0 +1,200 @@ +const { PrismaClient, GameV2, GameDetails } = require("@prisma/client"); +const ProgressBar = require('progress'); + +const prisma = new PrismaClient(); + +async function main() { + const games = await prisma.game.findMany({ + where: { + deleted: false, + parent_game_id: null, + }, + include: { + user: true, + favorite: true, + player_characters: { + include: { + role: true, + related_role: true, + }, + }, + demon_bluffs: { + include: { + role: true, + }, + }, + fabled: { + include: { + role: true, + }, + }, + grimoire: { + include: { + tokens: { + include: { + role: true, + related_role: true, + reminders: true, + player: true, + }, + }, + }, + }, + community: true, + associated_script: true, + }, + }); + + var bar = new ProgressBar(':current / :total :bar :percent', { total: games.length }); + + for (const game of games) { + await prisma.gameV2.create({ + data: { + id: game.id, + bgg_id: game.bgg_id, + created_at: game.created_at, + user: { + connect: { + user_id: game.user_id, + }, + }, + favorite: game.favorite + ? { + create: { + user: { + connect: { + user_id: game.favorite.user_id, + }, + }, + }, + } + : undefined, + characters: game.player_characters + ? { + createMany: { + data: game.player_characters.map((character) => ({ + name: character.name, + related: character.related, + alignment: character.alignment, + role_id: character.role_id, + related_role_id: character.related_role_id, + })), + }, + } + : undefined, + ignore_for_stats: game.ignore_for_stats, + tags: game.tags, + waiting_for_confirmation: game.waiting_for_confirmation, + details: { + create: { + date: game.date, + privacy: game.privacy, + tags: game.tags, + script: game.script, + location_type: game.location_type, + location: game.location, + player_count: game.player_count, + traveler_count: game.traveler_count, + win_v2: game.win_v2, + image_urls: game.image_urls, + associated_script: game.associated_script + ? { + connect: { + id: game.associated_script.id, + }, + } + : undefined, + community: game.community + ? { + connect: { + id: game.community_id, + }, + } + : undefined, + demon_bluffs: game.demon_bluffs + ? { + createMany: { + data: game.demon_bluffs.map((demonBluff) => ({ + name: demonBluff.name, + role_id: demonBluff.role_id, + related: demonBluff.related, + })), + }, + } + : undefined, + fabled: game.fabled + ? { + createMany: { + data: game.fabled.map((fabled) => ({ + name: fabled.name, + role_id: fabled.role_id, + related: fabled.related, + })), + }, + } + : undefined, + grimoire: game.grimoire + ? { + create: game.grimoire.map((grimoire) => ({ + created_at: grimoire.created_at, + tokens: grimoire.tokens + ? { + create: grimoire.tokens.map((token) => ({ + alignment: token.alignment, + is_dead: token.is_dead, + used_ghost_vote: token.used_ghost_vote, + order: token.order, + created_at: token.created_at, + player_name: token.player_name, + role: token.role + ? { + connect: { + id: token.role_id, + }, + } + : undefined, + related_role: token.related_role_id + ? { + connect: { + id: token.related_role_id, + }, + } + : undefined, + player: token.player + ? { + connect: { + user_id: token.player_id, + }, + } + : undefined, + reminders: token.reminders + ? { + createMany: { + data: token.reminders.map((reminder) => ({ + reminder: reminder.reminder, + token_url: reminder.token_url, + created_at: reminder.created_at, + })), + }, + } + : undefined, + })), + } + : undefined, + })), + } + : undefined, + }, + }, + }, + }); + + bar.tick(); + } +} + +main() + .catch(console.error) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/prisma/migrations/20241110162059_add_game_v2/migration.sql b/prisma/migrations/20241110162059_add_game_v2/migration.sql new file mode 100644 index 00000000..63cfc602 --- /dev/null +++ b/prisma/migrations/20241110162059_add_game_v2/migration.sql @@ -0,0 +1,170 @@ +/* + Warnings: + + - A unique constraint covering the columns `[game_v2_id]` on the table `FavoriteGame` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropForeignKey +ALTER TABLE "FavoriteGame" DROP CONSTRAINT "FavoriteGame_game_id_fkey"; + +-- AlterTable +ALTER TABLE "Character" ADD COLUMN "game_v2_id" INTEGER; + +-- AlterTable +ALTER TABLE "DemonBluff" ADD COLUMN "game_details_id" TEXT; + +-- AlterTable +ALTER TABLE "Fabled" ADD COLUMN "game_details_id" TEXT; + +-- AlterTable +ALTER TABLE "FavoriteGame" ADD COLUMN "game_v2_id" INTEGER, +ALTER COLUMN "game_id" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "ReminderToken" ADD COLUMN "token_v2_id" INTEGER; + +-- CreateTable +CREATE TABLE "GameV2" ( + "id" SERIAL NOT NULL, + "bgg_id" INTEGER, + "user_id" TEXT NOT NULL, + "game_id" TEXT NOT NULL, + "favorite_id" INTEGER, + "ignore_for_stats" BOOLEAN NOT NULL DEFAULT false, + "tags" TEXT[], + "waiting_for_confirmation" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "GameV2_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "GameDetails" ( + "id" TEXT NOT NULL, + "date" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP, + "privacy" "PrivacySetting" NOT NULL DEFAULT 'PUBLIC', + "tags" TEXT[], + "script" TEXT NOT NULL, + "script_id" INTEGER, + "location_type" "LocationType" NOT NULL DEFAULT 'ONLINE', + "location" TEXT NOT NULL, + "community_id" INTEGER, + "player_count" INTEGER, + "traveler_count" INTEGER, + "win_v2" "WinStatus_V2" NOT NULL DEFAULT 'NOT_RECORDED', + "image_urls" TEXT[], + + CONSTRAINT "GameDetails_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "GrimoireV2" ( + "id" SERIAL NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "GrimoireV2_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TokenV2" ( + "id" SERIAL NOT NULL, + "role_id" TEXT, + "related_role_id" TEXT, + "alignment" "Alignment" NOT NULL, + "is_dead" BOOLEAN NOT NULL DEFAULT false, + "used_ghost_vote" BOOLEAN NOT NULL DEFAULT false, + "order" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "grimoire_id" INTEGER NOT NULL, + "player_name" TEXT NOT NULL DEFAULT '', + "player_id" TEXT, + + CONSTRAINT "TokenV2_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_Storyteller" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_GameDetailsToGrimoireV2" ( + "A" TEXT NOT NULL, + "B" INTEGER NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "GameV2_bgg_id_key" ON "GameV2"("bgg_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "GameV2_favorite_id_key" ON "GameV2"("favorite_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "_Storyteller_AB_unique" ON "_Storyteller"("A", "B"); + +-- CreateIndex +CREATE INDEX "_Storyteller_B_index" ON "_Storyteller"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_GameDetailsToGrimoireV2_AB_unique" ON "_GameDetailsToGrimoireV2"("A", "B"); + +-- CreateIndex +CREATE INDEX "_GameDetailsToGrimoireV2_B_index" ON "_GameDetailsToGrimoireV2"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "FavoriteGame_game_v2_id_key" ON "FavoriteGame"("game_v2_id"); + +-- AddForeignKey +ALTER TABLE "FavoriteGame" ADD CONSTRAINT "FavoriteGame_game_id_fkey" FOREIGN KEY ("game_id") REFERENCES "Game"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FavoriteGame" ADD CONSTRAINT "FavoriteGame_game_v2_id_fkey" FOREIGN KEY ("game_v2_id") REFERENCES "GameV2"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "GameV2" ADD CONSTRAINT "GameV2_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "UserSettings"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "GameV2" ADD CONSTRAINT "GameV2_game_id_fkey" FOREIGN KEY ("game_id") REFERENCES "GameDetails"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "GameDetails" ADD CONSTRAINT "GameDetails_community_id_fkey" FOREIGN KEY ("community_id") REFERENCES "Community"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "GameDetails" ADD CONSTRAINT "GameDetails_script_id_fkey" FOREIGN KEY ("script_id") REFERENCES "Script"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TokenV2" ADD CONSTRAINT "TokenV2_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "Role"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TokenV2" ADD CONSTRAINT "TokenV2_related_role_id_fkey" FOREIGN KEY ("related_role_id") REFERENCES "Role"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TokenV2" ADD CONSTRAINT "TokenV2_grimoire_id_fkey" FOREIGN KEY ("grimoire_id") REFERENCES "GrimoireV2"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TokenV2" ADD CONSTRAINT "TokenV2_player_id_fkey" FOREIGN KEY ("player_id") REFERENCES "UserSettings"("user_id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ReminderToken" ADD CONSTRAINT "ReminderToken_token_v2_id_fkey" FOREIGN KEY ("token_v2_id") REFERENCES "TokenV2"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Character" ADD CONSTRAINT "Character_game_v2_id_fkey" FOREIGN KEY ("game_v2_id") REFERENCES "GameV2"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DemonBluff" ADD CONSTRAINT "DemonBluff_game_details_id_fkey" FOREIGN KEY ("game_details_id") REFERENCES "GameDetails"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Fabled" ADD CONSTRAINT "Fabled_game_details_id_fkey" FOREIGN KEY ("game_details_id") REFERENCES "GameDetails"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_Storyteller" ADD CONSTRAINT "_Storyteller_A_fkey" FOREIGN KEY ("A") REFERENCES "GameDetails"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_Storyteller" ADD CONSTRAINT "_Storyteller_B_fkey" FOREIGN KEY ("B") REFERENCES "UserSettings"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_GameDetailsToGrimoireV2" ADD CONSTRAINT "_GameDetailsToGrimoireV2_A_fkey" FOREIGN KEY ("A") REFERENCES "GameDetails"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_GameDetailsToGrimoireV2" ADD CONSTRAINT "_GameDetailsToGrimoireV2_B_fkey" FOREIGN KEY ("B") REFERENCES "GrimoireV2"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20241110185443_game_v2_id_type/migration.sql b/prisma/migrations/20241110185443_game_v2_id_type/migration.sql new file mode 100644 index 00000000..d7c748d2 --- /dev/null +++ b/prisma/migrations/20241110185443_game_v2_id_type/migration.sql @@ -0,0 +1,30 @@ +/* + Warnings: + + - The primary key for the `GameV2` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- DropForeignKey +ALTER TABLE "Character" DROP CONSTRAINT "Character_game_v2_id_fkey"; + +-- DropForeignKey +ALTER TABLE "FavoriteGame" DROP CONSTRAINT "FavoriteGame_game_v2_id_fkey"; + +-- AlterTable +ALTER TABLE "Character" ALTER COLUMN "game_v2_id" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "FavoriteGame" ALTER COLUMN "game_v2_id" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "GameV2" DROP CONSTRAINT "GameV2_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "GameV2_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "GameV2_id_seq"; + +-- AddForeignKey +ALTER TABLE "FavoriteGame" ADD CONSTRAINT "FavoriteGame_game_v2_id_fkey" FOREIGN KEY ("game_v2_id") REFERENCES "GameV2"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Character" ADD CONSTRAINT "Character_game_v2_id_fkey" FOREIGN KEY ("game_v2_id") REFERENCES "GameV2"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20241110185749_game_v2_add_deleted_column/migration.sql b/prisma/migrations/20241110185749_game_v2_add_deleted_column/migration.sql new file mode 100644 index 00000000..8a6347d1 --- /dev/null +++ b/prisma/migrations/20241110185749_game_v2_add_deleted_column/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "GameDetails" ADD COLUMN "deleted" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "GameV2" ADD COLUMN "deleted" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9c434c98..19b14964 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -68,9 +68,11 @@ model FavoriteGame { id Int @id @default(autoincrement()) user UserSettings @relation(fields: [user_id], references: [user_id]) user_id String - game Game @relation(fields: [game_id], references: [id]) - game_id String @unique + game Game? @relation(fields: [game_id], references: [id]) + game_id String? @unique created_at DateTime @default(now()) @db.Timestamptz(6) + game_v2 GameV2? @relation(fields: [game_v2_id], references: [id]) + game_v2_id String? @unique @@unique([user_id, game_id]) } @@ -107,13 +109,82 @@ model Token { reminders ReminderToken[] } +model GameV2 { + id String @id @default(uuid()) + bgg_id Int? @unique + user_id String + game_id String + favorite_id Int? @unique + characters Character[] + ignore_for_stats Boolean @default(false) + tags String[] + waiting_for_confirmation Boolean @default(false) + favorite FavoriteGame? + user UserSettings @relation(fields: [user_id], references: [user_id]) + details GameDetails @relation(fields: [game_id], references: [id]) + created_at DateTime? @default(now()) @db.Timestamptz(6) + deleted Boolean @default(false) +} + +model GameDetails { + id String @id @default(uuid()) + date DateTime @default(now()) @db.Date + privacy PrivacySetting @default(PUBLIC) + tags String[] + script String + script_id Int? + location_type LocationType @default(ONLINE) + location String + community Community? @relation(fields: [community_id], references: [id]) + community_id Int? + player_count Int? + traveler_count Int? + win_v2 WinStatus_V2 @default(NOT_RECORDED) + image_urls String[] + storytellers UserSettings[] @relation("Storyteller") + associated_script Script? @relation(fields: [script_id], references: [id]) + grimoire GrimoireV2[] + demon_bluffs DemonBluff[] + fabled Fabled[] + records GameV2[] + deleted Boolean @default(false) +} + +model GrimoireV2 { + id Int @id @default(autoincrement()) + created_at DateTime @default(now()) @db.Timestamptz(6) + tokens TokenV2[] + game GameDetails[] +} + +model TokenV2 { + id Int @id @default(autoincrement()) + role Role? @relation(fields: [role_id], references: [id]) + role_id String? + related_role Role? @relation("RelatedToken", fields: [related_role_id], references: [id]) + related_role_id String? + alignment Alignment + is_dead Boolean @default(false) + used_ghost_vote Boolean @default(false) + order Int + created_at DateTime @default(now()) @db.Timestamptz(6) + grimoire GrimoireV2 @relation(fields: [grimoire_id], references: [id]) + grimoire_id Int + player_name String @default("") + player UserSettings? @relation(fields: [player_id], references: [user_id]) + player_id String? + reminders ReminderToken[] +} + model ReminderToken { - id Int @id @default(autoincrement()) - reminder String @default("") - token_url String @default("") - created_at DateTime @default(now()) @db.Timestamptz(6) - token Token? @relation(fields: [token_id], references: [id]) - token_id Int? + id Int @id @default(autoincrement()) + reminder String @default("") + token_url String @default("") + created_at DateTime @default(now()) @db.Timestamptz(6) + token Token? @relation(fields: [token_id], references: [id]) + token_id Int? + token_v2 TokenV2? @relation(fields: [token_v2_id], references: [id]) + token_v2_id Int? } model Character { @@ -127,26 +198,32 @@ model Character { role_id String? related_role_id String? related_role Role? @relation("RelatedRole", fields: [related_role_id], references: [id]) + game_v2 GameV2? @relation(fields: [game_v2_id], references: [id]) + game_v2_id String? } model DemonBluff { - id Int @id @default(autoincrement()) - name String - role Role? @relation(fields: [role_id], references: [id]) - related String? - game Game? @relation(fields: [game_id], references: [id]) - game_id String? - role_id String? + id Int @id @default(autoincrement()) + name String + role Role? @relation(fields: [role_id], references: [id]) + related String? + game Game? @relation(fields: [game_id], references: [id]) + game_id String? + role_id String? + game_details GameDetails? @relation(fields: [game_details_id], references: [id]) + game_details_id String? } model Fabled { - id Int @id @default(autoincrement()) - name String - role Role? @relation(fields: [role_id], references: [id]) - related String? - game Game? @relation(fields: [game_id], references: [id]) - game_id String? - role_id String? + id Int @id @default(autoincrement()) + name String + role Role? @relation(fields: [role_id], references: [id]) + related String? + game Game? @relation(fields: [game_id], references: [id]) + game_id String? + role_id String? + game_details GameDetails? @relation(fields: [game_details_id], references: [id]) + game_details_id String? } model Script { @@ -168,6 +245,7 @@ model Script { roles Role[] games Game[] events Event[] + game_details GameDetails[] @@unique([script_id, version]) } @@ -188,6 +266,8 @@ model Role { demon_bluffs DemonBluff[] fabled Fabled[] reminders RoleReminder[] + related_tokens_v2 TokenV2[] @relation("RelatedToken") + tokens_v2 TokenV2[] } model RoleReminder { @@ -262,6 +342,9 @@ model UserSettings { favorites FavoriteGame[] created_events Event[] calendar SharedCalendarLink? + games_v2 GameV2[] + token_v2 TokenV2[] + storytold_games GameDetails[] @relation("Storyteller") } model Did { @@ -373,6 +456,7 @@ model Community { location String? city_id String? city City? @relation(fields: [city_id], references: [id]) + games_v2 GameDetails[] } model CommunityPost { diff --git a/server/api/games/v2/[id].delete.ts b/server/api/games/v2/[id].delete.ts new file mode 100644 index 00000000..52169b17 --- /dev/null +++ b/server/api/games/v2/[id].delete.ts @@ -0,0 +1,83 @@ +import type { User } from "@supabase/supabase-js"; +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const user: User | null = handler.context.user; + const gameId = handler.context.params?.id; + const { untag } = getQuery(handler) as { + untag: "true" | "false"; + }; + + if (!user) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + if (!gameId) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + const game = await prisma.game.findUnique({ + where: { + id: gameId, + user_id: user.id, + }, + select: { + user_id: true, + grimoire: { + include: { + tokens: true, + }, + }, + }, + }); + + if (!game || game.user_id !== user.id) { + throw createError({ + status: 404, + statusMessage: "Not Found", + }); + } + + // delete the game + await prisma.game.update({ + where: { + id: gameId, + }, + data: { + deleted: true, + deleted_date: new Date(), + }, + }); + + if (untag === "true") { + // If the player requests to be untagged from the game, we need to remove them from + // the grimoire. + // With game_v2, we'll also take them off of the storyteller list, but that's hard right now. + + for (const grimoire of game.grimoire) { + for (const token of grimoire.tokens) { + if (token.player_id === user.id) { + await prisma.token.update({ + where: { + id: token.id, + }, + data: { + player_id: null, + player_name: "", + }, + }); + } + } + } + } + + return game; +}); diff --git a/server/api/games/v2/[id].get.ts b/server/api/games/v2/[id].get.ts new file mode 100644 index 00000000..cfaeddc9 --- /dev/null +++ b/server/api/games/v2/[id].get.ts @@ -0,0 +1,16 @@ +import type { User } from "@supabase/supabase-js"; +import { fetchGame } from "~/server/utils/fetchGames"; + +export default defineEventHandler(async (handler) => { + const me: User | null = handler.context.user; + const gameId = handler.context.params?.id; + + if (!gameId) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + return fetchGame(gameId, me); +}); diff --git a/server/api/games/v2/[id].put.ts b/server/api/games/v2/[id].put.ts new file mode 100644 index 00000000..961df3c5 --- /dev/null +++ b/server/api/games/v2/[id].put.ts @@ -0,0 +1,518 @@ +import type { User } from "@supabase/supabase-js"; +import { + PrismaClient, + Game, + Character, + Grimoire, + Token, + Alignment, + DemonBluff, + Fabled, + ReminderToken, +} from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const user: User | null = handler.context.user; + const gameId = handler.context.params?.id; + const body = await readBody< + | (Game & { + player_characters: (Character & { role?: { token_url: string } })[]; + demon_bluffs: (DemonBluff & { role?: { token_url: string } })[]; + fabled: (Fabled & { role?: { token_url: string } })[]; + grimoire: Partial< + Grimoire & { + tokens: Partial< + Token & { + reminders: Partial[]; + } + >[]; + } + >[]; + }) + | null + >(handler); + + if (!user) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + if (!body) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + if (!gameId) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + const existingGame = await prisma.game.findUnique({ + where: { + id: gameId, + }, + include: { + player_characters: true, + demon_bluffs: true, + fabled: true, + }, + }); + + if (!existingGame || existingGame.user_id !== user.id) { + throw createError({ + status: 404, + statusMessage: "Not Found", + }); + } + + const game = await prisma.game.update({ + where: { + id: gameId, + }, + data: { + ...body, + date: new Date(body.date), + user_id: user.id, + player_characters: { + deleteMany: { + id: { + in: existingGame.player_characters.map((character) => character.id), + }, + }, + create: [...body.player_characters], + }, + demon_bluffs: { + deleteMany: { + id: { + in: existingGame.demon_bluffs.map((bluff) => bluff.id), + }, + }, + create: [...body.demon_bluffs], + }, + fabled: { + deleteMany: { + id: { + in: existingGame.fabled.map((fabled) => fabled.id), + }, + }, + create: [...body.fabled], + }, + grimoire: { + deleteMany: { + id: { + notIn: body.grimoire.filter((g) => !!g.id).map((g) => g.id!), + }, + }, + create: [ + ...body.grimoire + .filter((g) => !g.id) + .map((g) => ({ + tokens: { + create: g.tokens?.map((token, index) => ({ + role_id: token.role_id, + related_role_id: token.related_role_id, + alignment: token.alignment || Alignment.NEUTRAL, + is_dead: token.is_dead || false, + used_ghost_vote: token.used_ghost_vote || false, + order: token.order || index, + player_name: token.player_name || "", + player_id: token.player_id, + reminders: { + create: + token.reminders?.map((reminder) => ({ + reminder: reminder.reminder, + token_url: reminder.token_url, + })) || [], + }, + })), + }, + })), + ], + update: [ + ...body.grimoire + .filter((g) => g.id) + .map((g) => ({ + where: { + id: g.id, + }, + data: { + ...g, + tokens: { + deleteMany: { + id: { + notIn: g.tokens + ?.filter((token) => !!token.id) + .map((token) => token.id!), + }, + }, + create: g.tokens + ?.filter((token) => !token.id) + .map((token, index) => ({ + role_id: token.role_id, + related_role_id: token.related_role_id, + alignment: token.alignment || Alignment.NEUTRAL, + is_dead: token.is_dead || false, + used_ghost_vote: token.used_ghost_vote || false, + order: token.order || index, + player_name: token.player_name || "", + player_id: token.player_id, + reminders: { + create: + token.reminders?.map((reminder) => ({ + reminder: reminder.reminder, + token_url: reminder.token_url, + })) || [], + }, + })), + update: g.tokens + ?.filter((token) => token.id) + .map((token, index) => ({ + where: { + id: token.id, + }, + data: { + role_id: token.role_id ?? null, + related_role_id: token.related_role_id ?? null, + alignment: token.alignment || Alignment.NEUTRAL, + is_dead: token.is_dead || false, + used_ghost_vote: token.used_ghost_vote || false, + order: token.order || index, + player_name: token.player_name || "", + player_id: token.player_id ?? null, + reminders: { + deleteMany: { + id: { + notIn: token.reminders + ?.filter((reminder) => !!reminder.id) + .map((reminder) => reminder.id!), + }, + }, + create: token.reminders + ?.filter((reminder) => !reminder.id) + .map((reminder) => ({ + reminder: reminder.reminder, + token_url: reminder.token_url, + })), + update: token.reminders + ?.filter((reminder) => reminder.id) + .map((reminder) => ({ + where: { + id: reminder.id, + }, + data: { + reminder: reminder.reminder, + token_url: reminder.token_url, + }, + })), + }, + }, + })), + }, + }, + })), + ], + }, + }, + include: { + player_characters: true, + demon_bluffs: true, + fabled: true, + grimoire: { + include: { + tokens: { + include: { + role: true, + related_role: true, + reminders: true, + player: { + select: { + username: true, + display_name: true, + }, + }, + }, + }, + }, + }, + child_games: { + include: { + demon_bluffs: true, + fabled: true, + player_characters: true, + }, + }, + parent_game: { + include: { + player_characters: true, + demon_bluffs: true, + fabled: true, + child_games: { + include: { + player_characters: true, + demon_bluffs: true, + fabled: true, + }, + }, + }, + }, + community: { + select: { + slug: true, + icon: true, + }, + }, + associated_script: { + select: { + version: true, + script_id: true, + is_custom_script: true, + logo: true, + }, + }, + }, + }); + + const related_games = [...game.child_games]; + + if (game.parent_game) { + related_games.push(game.parent_game); + related_games.push(...game.parent_game.child_games); + } + + const taggedPlayers = new Set( + game.grimoire.flatMap((g) => g.tokens?.map((t) => t.player_id)) + ); + + for (const id of taggedPlayers) { + if (!id || id === user.id) continue; + + // Reduce grimoire to find all tokens that have this player_id + const player_characters = game.grimoire.reduce( + (acc, g) => { + const tokens = g.tokens?.filter((t) => t.player_id === id); + if (tokens) { + for (const token of tokens) { + // Don't add the token if it's identical to the last token + // in the player_characters array + + const lastToken = acc[acc.length - 1]; + if ( + lastToken && + lastToken.role_id === token.role_id && + lastToken.related_role_id === token.related_role_id + ) { + continue; + } + + acc.push({ + name: token.role?.name || "", + alignment: token.alignment, + related: token.related_role?.name || "", + role_id: token.role_id, + related_role_id: token.related_role_id, + }); + } + } + + return acc; + }, + [] as { + name: string; + alignment: Alignment; + related: string; + role_id: string | null; + related_role_id: string | null; + }[] + ); + + const relatedGame = related_games?.find((g) => g!.user_id === id); + + try { + if (!relatedGame) { + await prisma.game.create({ + data: { + ...body, + date: new Date(body.date), + user_id: id, + player_characters: { + create: [...player_characters], + }, + demon_bluffs: { + create: [...body.demon_bluffs], + }, + fabled: { + create: [...body.fabled], + }, + // map the already created grimoires to the new game + grimoire: { + connect: game.grimoire.map((g) => ({ id: g.id })), + }, + parent_game_id: game.parent_game_id || game.id, + waiting_for_confirmation: true, + is_storyteller: false, + }, + }); + } else { + await prisma.game.update({ + where: { + id: relatedGame.id, + }, + data: { + date: new Date(body.date), + script: body.script, + script_id: body.script_id, + location_type: body.location_type, + location: body.location, + community_name: body.community_name, + community_id: body.community_id, + player_count: body.player_count, + traveler_count: body.traveler_count, + storyteller: body.storyteller, + co_storytellers: body.co_storytellers, + win_v2: body.win_v2, + demon_bluffs: { + deleteMany: relatedGame.demon_bluffs.map((g) => ({ id: g.id })), + create: game.demon_bluffs.map((g) => ({ + ...g, + id: undefined, + game_id: undefined, + })), + }, + fabled: { + deleteMany: relatedGame.fabled.map((g) => ({ id: g.id })), + create: game.fabled.map((g) => ({ + ...g, + id: undefined, + game_id: undefined, + })), + }, + player_characters: { + deleteMany: relatedGame.player_characters.map((g) => ({ + id: g.id, + })), + create: player_characters, + }, + grimoire: { + connect: game.grimoire.map((g) => ({ id: g.id })), + }, + }, + }); + } + } catch (err: any) { + const messageLines = err.message.split("\n"); + const message = + messageLines[messageLines.length - 1].length > 0 + ? messageLines[messageLines.length - 1] + : err.message; + // get the name from the game.grimoire + const taggedPlayer = + game.grimoire.flatMap((g) => g.tokens).find((t) => t.player_id === id) + ?.player_name || "Unknown"; + + console.error(`Error saving for ${taggedPlayer}: ${message}`); + + throw createError({ + status: 500, + statusMessage: `Error saving for ${taggedPlayer}: ${message}`, + }); + } + } + + const storytellers = [game.storyteller, ...game.co_storytellers]; + + for (const storyteller of storytellers) { + if (storyteller?.includes("@")) { + // Verify that it's a friend + const friend = await prisma.userSettings.findUnique({ + where: { + username: storyteller.replace("@", ""), + friends: { + some: { + user_id: user.id, + }, + }, + }, + }); + + if (friend !== null) { + const childGame = game.child_games?.find( + (g) => g.user_id === friend.user_id + ); + + if (!childGame) { + await prisma.game.create({ + data: { + ...body, + is_storyteller: true, + date: new Date(body.date), + user_id: friend.user_id, + player_characters: {}, + demon_bluffs: { + create: [...body.demon_bluffs], + }, + fabled: { + create: [...body.fabled], + }, + notes: "", + grimoire: { + connect: game.grimoire.map((g) => ({ id: g.id })), + }, + parent_game_id: game.id, + waiting_for_confirmation: true, + tags: [], + }, + }); + } else { + await prisma.game.update({ + where: { + id: childGame.id, + }, + data: { + date: new Date(body.date), + script: body.script, + script_id: body.script_id, + location_type: body.location_type, + location: body.location, + community_name: body.community_name, + community_id: body.community_id, + player_count: body.player_count, + traveler_count: body.traveler_count, + storyteller: body.storyteller, + co_storytellers: body.co_storytellers, + win_v2: body.win_v2, + demon_bluffs: { + deleteMany: childGame.demon_bluffs.map((g) => ({ id: g.id })), + create: game.demon_bluffs.map((g) => ({ + ...g, + id: undefined, + game_id: undefined, + })), + }, + fabled: { + deleteMany: childGame.fabled.map((g) => ({ id: g.id })), + create: game.fabled.map((g) => ({ + ...g, + id: undefined, + game_id: undefined, + })), + }, + grimoire: { + connect: game.grimoire.map((g) => ({ id: g.id })), + }, + }, + }); + } + } + } + } + + return game; +}); diff --git a/server/api/games/v2/[id]/claim_seat.put.ts b/server/api/games/v2/[id]/claim_seat.put.ts new file mode 100644 index 00000000..67ac291d --- /dev/null +++ b/server/api/games/v2/[id]/claim_seat.put.ts @@ -0,0 +1,292 @@ +import type { User } from "@supabase/supabase-js"; +import { Alignment, PrismaClient } from "@prisma/client"; +import { fetchGame } from "~/server/utils/fetchGames"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const user: User | null = handler.context.user; + const gameId = handler.context.params?.id as string; + const body = await readBody<{ + order: number; + }>(handler); + + if (!user) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + if (!body || !body.order) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + const userDetails = await prisma.userSettings.findUnique({ + where: { + user_id: user.id, + }, + }); + + if (!userDetails) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + /** Reasons to disallow claimining the seat: + * 2. The user is not a friend of the game creator + * 3. The user is not in a community with the game creator + * 4. The user is a storyteller + * 5. The user is a co-storyteller + * 6. The user already has a seat in the grimoire + */ + + const game = await prisma.game.findUnique({ + where: { + id: gameId, + OR: [ + { + user: { + friends: { + some: { + friend_id: user.id, + }, + }, + }, + }, + { + community: { + members: { + some: { + user_id: user.id, + }, + }, + }, + }, + ], + storyteller: { + not: `@${userDetails.username}`, + }, + AND: { + OR: [ + { + NOT: { + co_storytellers: { + has: `@${userDetails.username}`, + }, + }, + }, + { + co_storytellers: { + isEmpty: true, + }, + }, + { + co_storytellers: { + equals: null, + }, + }, + ], + }, + grimoire: { + some: { + tokens: { + none: { + player_id: user.id, + }, + }, + }, + }, + }, + include: { + player_characters: true, + demon_bluffs: true, + fabled: true, + grimoire: { + include: { + tokens: { + include: { + role: true, + related_role: true, + reminders: true, + player: { + select: { + username: true, + display_name: true, + }, + }, + }, + }, + }, + }, + child_games: { + include: { + demon_bluffs: true, + fabled: true, + player_characters: true, + }, + }, + parent_game: { + include: { + player_characters: true, + demon_bluffs: true, + fabled: true, + child_games: { + include: { + player_characters: true, + demon_bluffs: true, + fabled: true, + }, + }, + }, + }, + community: { + select: { + slug: true, + icon: true, + }, + }, + associated_script: { + select: { + version: true, + script_id: true, + is_custom_script: true, + logo: true, + }, + }, + }, + }); + + if (!game) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + // Update the grimoire to tag the user as the player in the correct seat + + await prisma.token.updateMany({ + where: { + order: body.order, + grimoire: { + game: { + some: { + id: gameId, + }, + }, + }, + }, + data: { + player_id: user.id, + player_name: userDetails.display_name, + }, + }); + + // Reduce grimoire to find all tokens that have this player_id + const player_characters = game.grimoire.reduce( + (acc, g) => { + const tokens = g.tokens?.filter((t) => t.player_id === user.id); + if (tokens) { + for (const token of tokens) { + // Don't add the token if it's identical to the last token + // in the player_characters array + + const lastToken = acc[acc.length - 1]; + if ( + lastToken && + lastToken.role_id === token.role_id && + lastToken.related_role_id === token.related_role_id + ) { + continue; + } + + acc.push({ + name: token.role?.name || "", + alignment: token.alignment, + related: token.related_role?.name || "", + role_id: token.role_id, + related_role_id: token.related_role_id, + }); + } + } + + return acc; + }, + [] as { + name: string; + alignment: Alignment; + related: string; + role_id: string | null; + related_role_id: string | null; + }[] + ); + + try { + await prisma.game.create({ + data: { + ...game, + id: undefined, + community: undefined, + associated_script: undefined, + user: undefined, + parent_game: undefined, + parent_game_id: game.parent_game_id || game.id, + child_games: undefined, + user_id: user.id, + player_characters: { + create: [...player_characters], + }, + demon_bluffs: { + create: [ + ...game.demon_bluffs.map((d) => ({ + ...d, + id: undefined, + game_id: undefined, + })), + ], + }, + fabled: { + create: [ + ...game.fabled.map((f) => ({ + ...f, + id: undefined, + game_id: undefined, + })), + ], + }, + // map the already created grimoires to the new game + grimoire: { + connect: game.grimoire.map((g) => ({ id: g.id })), + }, + waiting_for_confirmation: false, + is_storyteller: false, + }, + }); + } catch (err: any) { + const messageLines = err.message.split("\n"); + const message = + messageLines[messageLines.length - 1].length > 0 + ? messageLines[messageLines.length - 1] + : err.message; + // get the name from the game.grimoire + const taggedPlayer = + game.grimoire + .flatMap((g) => g.tokens) + .find((t) => t.player_id === user.id)?.player_name || "Unknown"; + + console.error(`Error saving for ${taggedPlayer}: ${message}`); + + throw createError({ + status: 500, + statusMessage: `Error saving for ${taggedPlayer}: ${message}`, + }); + } + + return fetchGame(gameId, user, false); +}); diff --git a/server/api/games/v2/[id]/confirm.post.ts b/server/api/games/v2/[id]/confirm.post.ts new file mode 100644 index 00000000..610d0c1b --- /dev/null +++ b/server/api/games/v2/[id]/confirm.post.ts @@ -0,0 +1,47 @@ +import type { User } from "@supabase/supabase-js"; +import { PrismaClient } from "@prisma/client"; +import { fetchGame } from "~/server/utils/fetchGames"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const user: User | null = handler.context.user; + + if (!user) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + const gameId = handler.context.params?.id; + + const game = await prisma.game.findUnique({ + where: { + id: gameId, + user_id: user.id, + deleted: false, + }, + }); + + if (!game) { + throw createError({ + status: 404, + statusMessage: "Not Found", + }); + } + + await prisma.game.update({ + where: { + id: game.id, + }, + data: { + waiting_for_confirmation: false, + }, + select: { + waiting_for_confirmation: true, + }, + }); + + return fetchGame(game.id, user); +}); diff --git a/server/api/games/v2/[id]/favorite.post.ts b/server/api/games/v2/[id]/favorite.post.ts new file mode 100644 index 00000000..fd623473 --- /dev/null +++ b/server/api/games/v2/[id]/favorite.post.ts @@ -0,0 +1,93 @@ +import { FavoriteGame, PrismaClient } from "@prisma/client"; +import { User } from "@supabase/supabase-js"; + +const prisma = new PrismaClient(); + +export default defineEventHandler( + async ( + handler + ): Promise<{ + status: "deleted" | "added"; + data: { + id: number; + game_id: string; + }; + }> => { + const user: User | null = handler.context.user; + const gameId = handler.context.params?.id; + + if (!user) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + const game = await prisma.game.findUnique({ + where: { + id: gameId, + }, + select: { + id: true, + user_id: true, + }, + }); + + if (!game) { + throw createError({ + status: 404, + statusMessage: "Not Found", + }); + } + + if (game.user_id !== user.id) { + throw createError({ + status: 403, + statusMessage: "Forbidden", + }); + } + + // Check if it's already a favorite + const existingFavorite = await prisma.favoriteGame.findFirst({ + where: { + user_id: user.id, + game_id: game.id, + }, + select: { + id: true, + game_id: true, + }, + }); + + if (existingFavorite) { + // delete it + await prisma.favoriteGame.delete({ + where: { + id: existingFavorite.id, + }, + }); + + return { + status: "deleted", + data: existingFavorite, + }; + } else { + // add it + const favorite = await prisma.favoriteGame.create({ + data: { + user_id: user.id, + game_id: game.id, + }, + select: { + id: true, + game_id: true, + }, + }); + + return { + status: "added", + data: favorite, + }; + } + } +); diff --git a/server/api/games/v2/[id]/merge.post.ts b/server/api/games/v2/[id]/merge.post.ts new file mode 100644 index 00000000..88922da0 --- /dev/null +++ b/server/api/games/v2/[id]/merge.post.ts @@ -0,0 +1,102 @@ +import type { User } from "@supabase/supabase-js"; +import { PrismaClient, PrivacySetting } from "@prisma/client"; +import { fetchGame } from "~/server/utils/fetchGames"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const gameId = handler.context.params?.id; + const me: User | null = handler.context.user; + const { id: gameToMergeWith } = await readBody<{ id: string }>(handler); + + if (!gameId || !me) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + /** + * Problem: We have two games. We want one. + * Original solution: Update a few details in `gameToMergeWith` and then delete `game`. + * Problem with original solution: We sometimes lose the grimoire. This is _bad_. + * + * New solution: Keep `game`, update a few details from `gameToMergeWith`, and then delete `gameToMergeWith`. + * + * Steps: + * 1. Fetch `game` and `gameToMergeWith` + */ + + // Fetch the parent game from the existing child game + const game = await prisma.game.findUnique({ + where: { + id: gameId, + user_id: me.id, + deleted: false, + }, + select: { + tags: true, + }, + }); + + // If somehow there isn't a parent game, throw + if (!game) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + // Fetch the gameToMergeWith details we care about + // notes, attachments, privacy. + + const gameToMergeWithDetails = await prisma.game.findUnique({ + where: { + id: gameToMergeWith, + user_id: me.id, + deleted: false, + }, + select: { + id: true, + notes: true, + image_urls: true, + privacy: true, + tags: true, + }, + }); + + // If somehow there isn't a gameToMergeWith, throw + if (!gameToMergeWithDetails) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + // Update the game with the details from gameToMergeWith + await prisma.game.update({ + where: { + id: gameId, + }, + data: { + notes: gameToMergeWithDetails.notes, + image_urls: gameToMergeWithDetails.image_urls, + privacy: gameToMergeWithDetails.privacy, + tags: [...gameToMergeWithDetails.tags, ...game.tags], + waiting_for_confirmation: false, + }, + }); + + // Delete the gameToMergeWith + await prisma.game.update({ + where: { + id: gameToMergeWith, + }, + data: { + deleted: true, + }, + }); + + // Return the newly merged game + return fetchGame(gameId, me); +}); diff --git a/server/api/games/v2/[id]/minimal.ts b/server/api/games/v2/[id]/minimal.ts new file mode 100644 index 00000000..5c449575 --- /dev/null +++ b/server/api/games/v2/[id]/minimal.ts @@ -0,0 +1,157 @@ +import type { User } from "@supabase/supabase-js"; +import { PrismaClient, PrivacySetting } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const gameId = handler.context.params?.id; + const me: User | null = handler.context.user; + + if (!gameId) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + const game = await prisma.game.findUnique({ + where: { + id: gameId, + deleted: false, + OR: [ + // PUBLIC PROFILE + // PUBLIC GAME + { + user: { + privacy: PrivacySetting.PUBLIC, + }, + privacy: PrivacySetting.PUBLIC, + }, + // PRIVATE GAME + // Direct links are allowed, so we aren't performing any checks on the user + { + user: { + privacy: PrivacySetting.PUBLIC, + }, + privacy: PrivacySetting.PRIVATE, + }, + // FRIENDS ONLY GAME + { + user: { + privacy: PrivacySetting.PUBLIC, + OR: [ + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + { + user_id: me?.id || "", + }, + ], + }, + privacy: PrivacySetting.FRIENDS_ONLY, + }, + // PRIVATE PROFILE + // PUBLIC GAME + // No filtering done here, because the game is public. + // We'll need to anonymize the user's profile, though. + { + user: { + privacy: PrivacySetting.PRIVATE, + }, + privacy: PrivacySetting.PUBLIC, + }, + // PRIVATE GAME + // Direct links are allowed, so we aren't performing any checks on the user + { + user: { + privacy: PrivacySetting.PRIVATE, + }, + privacy: PrivacySetting.PRIVATE, + }, + // FRIENDS ONLY GAME + { + user: { + privacy: PrivacySetting.PRIVATE, + OR: [ + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + { + user_id: me?.id || "", + }, + ], + }, + privacy: PrivacySetting.FRIENDS_ONLY, + }, + // FRIENDS ONLY PROFILE + // PUBLIC GAME + // No filtering done here, because the game is public. + // We'll need to anonymize the user's profile, though. + { + user: { + privacy: PrivacySetting.FRIENDS_ONLY, + }, + privacy: PrivacySetting.PUBLIC, + }, + // PRIVATE GAME + // Direct links are allowed, so we aren't performing any checks on the user + { + user: { + privacy: PrivacySetting.FRIENDS_ONLY, + }, + privacy: PrivacySetting.PRIVATE, + }, + // FRIENDS ONLY GAME + { + user: { + privacy: PrivacySetting.FRIENDS_ONLY, + OR: [ + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + { + user_id: me?.id || "", + }, + ], + }, + privacy: PrivacySetting.FRIENDS_ONLY, + }, + ], + }, + select: { + date: true, + script: true, + user: { + select: { + display_name: true, + }, + }, + associated_script: { + select: { + logo: true, + }, + }, + }, + }); + + if (!game) { + throw createError({ + status: 404, + statusMessage: "Not Found", + }); + } + + return game; +}); diff --git a/server/api/games/v2/[id]/post_to_bgg.delete.ts b/server/api/games/v2/[id]/post_to_bgg.delete.ts new file mode 100644 index 00000000..fad132bd --- /dev/null +++ b/server/api/games/v2/[id]/post_to_bgg.delete.ts @@ -0,0 +1,102 @@ +import type { User } from "@supabase/supabase-js"; +import { Alignment, PrismaClient, PrivacySetting } from "@prisma/client"; +import axios from "axios"; +// @ts-ignore +import dayjs from "dayjs"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const user = handler.context.user as User | null; + const gameId = handler.context.params?.id as string; + + if (!user) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + const userSettings = await prisma.userSettings.findUnique({ + where: { + user_id: user.id, + }, + select: { + bgg_cookies: true, + bgg_username: true, + privacy: true, + }, + }); + + if (!userSettings?.bgg_cookies || !userSettings?.bgg_username) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + const game = await prisma.game.findUnique({ + where: { + id: gameId, + user_id: user.id, + deleted: false, + }, + select: { + bgg_id: true, + }, + }); + + if (!game) { + throw createError({ + status: 404, + statusMessage: "Not Found", + }); + } + + const response = await axios.post<{ + success: boolean; + }>( + "https://boardgamegeek.com/geekplay.php", + { + playid: game.bgg_id, + ajax: 1, + finalize: 1, + action: "delete", + }, + { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Cookie: userSettings.bgg_cookies, + }, + } + ); + + await prisma.game.update({ + where: { + id: gameId, + }, + data: { + bgg_id: null, + }, + }); + + // update the cookies + const cookies = [ + response.headers["set-cookie"]?.[0], + response.headers["set-cookie"]?.[1], + ].filter((c) => !!c) as string[]; + + if (cookies[0] && cookies[1]) { + await prisma.userSettings.update({ + where: { + user_id: user.id, + }, + data: { + bgg_cookies: cookies, + }, + }); + } + + return true; +}); diff --git a/server/api/games/v2/[id]/post_to_bgg.post.ts b/server/api/games/v2/[id]/post_to_bgg.post.ts new file mode 100644 index 00000000..ccaaa639 --- /dev/null +++ b/server/api/games/v2/[id]/post_to_bgg.post.ts @@ -0,0 +1,194 @@ +import type { User } from "@supabase/supabase-js"; +import { + Alignment, + PrismaClient, + PrivacySetting, + WinStatus_V2, +} from "@prisma/client"; +import axios from "axios"; +// @ts-ignore +import dayjs from "dayjs"; +import { fetchGame } from "~/server/utils/fetchGames"; +import { useFeatureFlags } from "~/server/utils/featureFlags"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const user = handler.context.user as User | null; + const gameId = handler.context.params?.id as string; + const body = await readBody<{ anonymize: boolean }>(handler); + + if (!user) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + if (!body) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + const userSettings = await prisma.userSettings.findUnique({ + where: { + user_id: user.id, + }, + select: { + display_name: true, + bgg_cookies: true, + bgg_username: true, + privacy: true, + }, + }); + + if (!userSettings?.bgg_cookies || !userSettings?.bgg_username) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + const game = await fetchGame(gameId, user, true); + + if (!game) { + throw createError({ + status: 404, + statusMessage: "Not Found", + }); + } + + const storyteller = game.storyteller?.startsWith("@") + ? (await prisma.userSettings + .findUnique({ + where: { + username: game.storyteller.slice(1), + }, + select: { + display_name: true, + }, + }) + .then((u) => u?.display_name)) ?? game.storyteller + : game.storyteller; + + let location = game.location || game.location_type; + const players: { + username: string | null; + name: string; + win: boolean; + color: string; + }[] = []; + + if (storyteller) { + players.push({ + username: null, + name: storyteller, + win: false, + color: "Storyteller", + }); + } else if (game.is_storyteller) { + players.push({ + username: userSettings.bgg_username, + name: userSettings.display_name, + win: false, + color: "Storyteller", + }); + } + const parentGameLastAlignment = + game.player_characters[game.player_characters.length - 1]?.alignment || + Alignment.NEUTRAL; + for (const token of game.grimoire[game.grimoire.length - 1].tokens) { + if (token.player_name) { + players.push({ + username: null, + name: token.player_name, + win: await (async () => { + return token.alignment === Alignment.GOOD + ? game.win_v2 === WinStatus_V2.GOOD_WINS + : game.win_v2 === WinStatus_V2.EVIL_WINS; + })(), + color: + token.alignment === Alignment.GOOD + ? "Good - " + token.role?.name + : token.alignment === Alignment.EVIL + ? "Evil -" + token.role?.name + : "", + }); + } + } + + // Remove @ sign + for (const player of players) { + player.name = player.name.replace("@", ""); + } + + // Anonymize the players if the user has that setting enabled + if (body.anonymize) { + // Reduce the player name to the first letter of their name + for (const player of players) { + player.name = player.name[0] + "."; + } + } + + const playResponse = await axios.post<{ + playid: string; + }>( + "https://boardgamegeek.com/geekplay.php", + { + playdate: dayjs(game.date).format("YYYY-MM-DD"), + comments: `${game.script} + +${game.notes} +`, + length: 60, + twitter: "false", + minutes: 60, + location: body.anonymize ? location[0] + "." : location, + objectid: "240980", + hours: 0, + quantity: "1", + action: "save", + date: game.date.toISOString(), + players, + objecttype: "thing", + ajax: 1, + }, + { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Cookie: userSettings.bgg_cookies, + }, + } + ); + + await prisma.game.update({ + where: { + id: gameId, + }, + data: { + bgg_id: +playResponse.data.playid, + }, + }); + + // update the cookies + const cookies = [ + playResponse.headers["set-cookie"]?.[0], + playResponse.headers["set-cookie"]?.[1], + ].filter((c) => !!c) as string[]; + + if (cookies[0] && cookies[1]) { + await prisma.userSettings.update({ + where: { + user_id: user.id, + }, + data: { + bgg_cookies: cookies, + }, + }); + } + + return true; +}); diff --git a/server/api/games/v2/[id]/similar.get.ts b/server/api/games/v2/[id]/similar.get.ts new file mode 100644 index 00000000..838b4dd1 --- /dev/null +++ b/server/api/games/v2/[id]/similar.get.ts @@ -0,0 +1,201 @@ +import type { User } from "@supabase/supabase-js"; +import { PrismaClient, PrivacySetting } from "@prisma/client"; +import { fetchGame } from "~/server/utils/fetchGames"; +import dayjs from "dayjs"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const me: User | null = handler.context.user; + const gameId = handler.context.params?.id; + + if (!gameId) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + const game = await fetchGame(gameId, me); + + if (!game || !me) { + throw createError({ + status: 404, + statusMessage: "Not Found", + }); + } + + const player_names: string[] = [ + ...new Set( + game.grimoire.flatMap(({ tokens }) => + tokens.map((token) => token.player_name) + ) + ), + ]; + + const similar_games = await prisma.game.findMany({ + where: { + id: { + not: gameId, + }, + deleted: false, + user_id: me.id, + date: { + lte: dayjs(game.date).add(1, "day").toDate(), + gte: dayjs(game.date).subtract(1, "day").toDate(), + }, + parent_game_id: null, + OR: [ + { + community_name: { + equals: game.community_name, + mode: "insensitive", + }, + }, + { + location: { + equals: game.location, + mode: "insensitive", + }, + }, + { + storyteller: { + equals: game.storyteller, + mode: "insensitive", + }, + }, + { + script: { + contains: game.script, + mode: "insensitive", + }, + }, + { + grimoire: { + some: { + tokens: { + some: { + player_name: { + in: player_names, + mode: "insensitive", + }, + }, + }, + }, + }, + }, + { + AND: { + OR: [ + { + player_count: game.player_count, + }, + { + player_count: null, + }, + ], + }, + }, + { + AND: { + OR: [ + { + traveler_count: game.traveler_count, + }, + { + traveler_count: null, + }, + ], + }, + }, + ], + }, + include: { + user: { + select: { + username: true, + }, + }, + player_characters: { + include: { + role: { + select: { + token_url: true, + type: true, + initial_alignment: true, + }, + }, + related_role: { + select: { + token_url: true, + }, + }, + }, + }, + demon_bluffs: { + include: { + role: { + select: { + token_url: true, + type: true, + }, + }, + }, + }, + fabled: { + include: { + role: { + select: { + token_url: true, + }, + }, + }, + }, + grimoire: { + include: { + tokens: { + include: { + role: true, + related_role: true, + reminders: true, + player: { + select: { + username: true, + display_name: true, + }, + }, + }, + }, + }, + orderBy: { + id: "asc", + }, + }, + parent_game: { + select: { + user: { + select: { + username: true, + display_name: true, + }, + }, + }, + }, + community: { + select: { + slug: true, + }, + }, + associated_script: { + select: { + version: true, + script_id: true, + is_custom_script: true, + logo: true, + }, + }, + }, + }); + + return similar_games; +}); diff --git a/server/api/games/v2/community/[slug].get.ts b/server/api/games/v2/community/[slug].get.ts new file mode 100644 index 00000000..5205ae56 --- /dev/null +++ b/server/api/games/v2/community/[slug].get.ts @@ -0,0 +1,117 @@ +import { PrismaClient, PrivacySetting } from "@prisma/client"; +import { User } from "@supabase/supabase-js"; +import { GameRecord } from "~/server/utils/anonymizeGame"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const me: User | null = handler.context.user; + const slug = handler.context.params?.slug as string; + + const community = await prisma.community.findUnique({ + where: { + slug, + }, + select: { + id: true, + is_private: true, + members: { + select: { + user_id: true, + }, + }, + banned_users: { + select: { + user_id: true, + }, + }, + }, + }); + + if (!community) { + console.error(`Community not found: ${slug}`); + throw createError({ + status: 404, + statusMessage: "Not Found", + }); + } + + if ( + community.banned_users.some((banned_user) => banned_user.user_id === me?.id) + ) { + console.error( + `User ${me?.id} is banned from community ${slug} (${community.id})` + ); + throw createError({ + status: 403, + statusMessage: "Forbidden", + }); + } + + // If the community is private, only members can see the games + // But it's not an error. + if ( + community.is_private && + !community.members.some((member) => member.user_id === me?.id) + ) { + return []; + } + + const games = await prisma.game.findMany({ + where: { + deleted: false, + community_id: community.id, + privacy: PrivacySetting.PUBLIC, + parent_game_id: null, + }, + include: { + user: { + select: { + privacy: true, + username: true, + }, + }, + player_characters: { + include: { + role: { + select: { + token_url: true, + type: true, + initial_alignment: true, + }, + }, + related_role: { + select: { + token_url: true, + }, + }, + }, + }, + associated_script: { + select: { + version: true, + script_id: true, + is_custom_script: true, + logo: true, + }, + }, + }, + orderBy: { + date: "desc", + }, + }); + + const anonymizedGames: GameRecord[] = []; + + for (const game of games) { + anonymizedGames.push( + await anonymizeGame( + { ...game, grimoire: [], demon_bluffs: [], fabled: [] } as GameRecord, + me, + false // always default to anonymous when loading on a community page + ) + ); + } + + return anonymizedGames; +}); diff --git a/server/api/games/v2/index.post.ts b/server/api/games/v2/index.post.ts new file mode 100644 index 00000000..122f04b2 --- /dev/null +++ b/server/api/games/v2/index.post.ts @@ -0,0 +1,246 @@ +import type { User } from "@supabase/supabase-js"; +import { + PrismaClient, + Game, + Character, + Token, + Grimoire, + Alignment, + DemonBluff, + Fabled, +} from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const user: User | null = handler.context.user; + const body = await readBody< + | (Game & { + player_characters: (Character & { role?: { token_url: string } })[]; + demon_bluffs: (DemonBluff & { role?: { token_url: string } })[]; + fabled: (Fabled & { role?: { token_url: string } })[]; + grimoire: Partial< + Grimoire & { + tokens: Partial< + Token & { + reminders: Partial<{ reminder: string; token_url: string }>[]; + } + >[]; + } + >[]; + }) + | null + >(handler); + + if (!user) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + if (!body) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + const newGame = await prisma.game.create({ + data: { + ...body, + date: new Date(body.date), + user_id: user.id, + player_characters: { + create: [...body.player_characters], + }, + demon_bluffs: { + create: [...body.demon_bluffs], + }, + fabled: { + create: [...body.fabled], + }, + grimoire: { + create: [ + ...body.grimoire.map((g) => ({ + ...g, + tokens: { + create: g.tokens?.map((token, index) => ({ + role_id: token.role_id, + related_role_id: token.related_role_id, + alignment: token.alignment || Alignment.NEUTRAL, + is_dead: token.is_dead || false, + used_ghost_vote: token.used_ghost_vote || false, + order: token.order || index, + player_name: token.player_name || "", + player_id: token.player_id, + reminders: { + create: + token.reminders?.map((reminder) => ({ + reminder: reminder.reminder, + token_url: reminder.token_url, + })) || [], + }, + })), + }, + })), + ], + }, + }, + include: { + user: { + select: { + username: true, + }, + }, + player_characters: true, + grimoire: { + include: { + tokens: { + include: { + role: true, + related_role: true, + reminders: true, + player: { + select: { + username: true, + display_name: true, + }, + }, + }, + }, + }, + }, + associated_script: { + select: { + version: true, + script_id: true, + is_custom_script: true, + logo: true, + }, + }, + }, + }); + + const taggedPlayers = new Set( + newGame.grimoire.flatMap((g) => g.tokens?.map((t) => t.player_id)), + ); + + for (const id of taggedPlayers) { + if (!id || id === user.id) continue; + + // Reduce grimoire to find all tokens that have this player_id + const player_characters = newGame.grimoire.reduce( + (acc, g) => { + const tokens = g.tokens?.filter((t) => t.player_id === id); + if (tokens) { + for (const token of tokens) { + // Don't add the token if it's identical to the last token + // in the player_characters array + + const lastToken = acc[acc.length - 1]; + if ( + lastToken && + lastToken.role_id === token.role_id && + lastToken.related_role_id === token.related_role_id + ) { + continue; + } + + acc.push({ + name: token.role?.name || "", + alignment: token.alignment, + related: token.related_role?.name || "", + role_id: token.role_id, + related_role_id: token.related_role_id, + }); + } + } + + return acc; + }, + [] as { + name: string; + alignment: Alignment; + related: string; + role_id: string | null; + related_role_id: string | null; + }[], + ); + + await prisma.game.create({ + data: { + ...body, + is_storyteller: false, + storyteller: + newGame.is_storyteller && newGame.user + ? `@${newGame.user.username}` + : "", + date: new Date(body.date), + user_id: id, + player_characters: { + create: [...player_characters], + }, + demon_bluffs: { + create: [...body.demon_bluffs], + }, + fabled: { + create: [...body.fabled], + }, + notes: "", + // map the already created grimoires to the new game + grimoire: { + connect: newGame.grimoire.map((g) => ({ id: g.id })), + }, + parent_game_id: newGame.id, + waiting_for_confirmation: true, + tags: [], + }, + }); + } + + const storytellers = [newGame.storyteller, ...newGame.co_storytellers]; + + for (const storyteller of storytellers) { + if (storyteller?.includes("@")) { + // Verify that it's a friend + const friend = await prisma.userSettings.findUnique({ + where: { + username: storyteller.replace("@", ""), + friends: { + some: { + user_id: user.id, + }, + }, + }, + }); + + if (friend !== null) { + await prisma.game.create({ + data: { + ...body, + is_storyteller: true, + date: new Date(body.date), + user_id: friend.user_id, + player_characters: {}, + demon_bluffs: { + create: [...body.demon_bluffs], + }, + fabled: { + create: [...body.fabled], + }, + notes: "", + grimoire: { + connect: newGame.grimoire.map((g) => ({ id: g.id })), + }, + parent_game_id: newGame.id, + waiting_for_confirmation: true, + tags: [], + }, + }); + } + } + } + + return newGame; +}); diff --git a/server/api/games/v2/index.put.ts b/server/api/games/v2/index.put.ts new file mode 100644 index 00000000..764fdb00 --- /dev/null +++ b/server/api/games/v2/index.put.ts @@ -0,0 +1,395 @@ +import type { User } from "@supabase/supabase-js"; +import { + PrismaClient, + Alignment, + LocationType, + WinStatus_V2, + PrivacySetting, +} from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const user: User | null = handler.context.user; + const body = await readBody<{ + game_ids: string[]; + script: string; + script_id: number | null; + storyteller: string; + co_storytellers: string[]; + is_storyteller: boolean; + location_type: LocationType | undefined; + location: string; + community_name: string; + community_id: number | null; + player_count: number | null; + traveler_count: number | null; + win_v2: WinStatus_V2 | undefined; + tags: string[]; + privacy: PrivacySetting | ""; + } | null>(handler); + + if (!user) { + throw createError({ + status: 401, + statusMessage: "Unauthorized", + }); + } + + if (!body) { + throw createError({ + status: 400, + statusMessage: "Bad Request", + }); + } + + const games = await prisma.game.findMany({ + where: { + id: { + in: body.game_ids, + }, + user_id: user.id, + }, + }); + + for (const payload of games) { + const game = await prisma.game.update({ + where: { + id: payload.id, + }, + data: { + script: body.script || payload.script, + script_id: body.script_id || payload.script_id, + storyteller: body.storyteller || payload.storyteller, + co_storytellers: + body.co_storytellers.length > 0 + ? body.co_storytellers + : payload.co_storytellers, + is_storyteller: body.is_storyteller || payload.is_storyteller, + location_type: body.location_type || payload.location_type, + location: body.location || payload.location, + community_name: body.community_name || payload.community_name, + community_id: body.community_id || payload.community_id, + player_count: body.player_count || payload.player_count, + traveler_count: body.traveler_count || payload.traveler_count, + win_v2: body.win_v2 || payload.win_v2, + tags: [...payload.tags, ...body.tags], + privacy: body.privacy || payload.privacy, + }, + include: { + player_characters: true, + demon_bluffs: true, + fabled: true, + grimoire: { + include: { + tokens: { + include: { + role: true, + related_role: true, + reminders: true, + player: { + select: { + username: true, + display_name: true, + }, + }, + }, + }, + }, + }, + child_games: { + include: { + demon_bluffs: true, + fabled: true, + player_characters: true, + }, + }, + parent_game: { + include: { + player_characters: true, + demon_bluffs: true, + fabled: true, + child_games: { + include: { + player_characters: true, + demon_bluffs: true, + fabled: true, + }, + }, + }, + }, + community: { + select: { + slug: true, + icon: true, + }, + }, + associated_script: { + select: { + version: true, + script_id: true, + is_custom_script: true, + logo: true, + }, + }, + }, + }); + + console.log({ + storyteller: game.storyteller, + co_storytellers: game.co_storytellers, + is_storyteller: game.is_storyteller, + location_type: game.location_type, + location: game.location, + community_name: game.community_name, + community_id: game.community_id, + player_count: game.player_count, + traveler_count: game.traveler_count, + win_v2: game.win_v2, + tags: game.tags, + privacy: game.privacy, + }); + + const related_games = [...game.child_games]; + + if (game.parent_game) { + related_games.push(game.parent_game); + related_games.push(...game.parent_game.child_games); + } + + const taggedPlayers = new Set( + game.grimoire.flatMap((g) => g.tokens?.map((t) => t.player_id)) + ); + + for (const id of taggedPlayers) { + if (!id || id === user.id) continue; + + // Reduce grimoire to find all tokens that have this player_id + const player_characters = game.grimoire.reduce( + (acc, g) => { + const tokens = g.tokens?.filter((t) => t.player_id === id); + if (tokens) { + for (const token of tokens) { + // Don't add the token if it's identical to the last token + // in the player_characters array + + const lastToken = acc[acc.length - 1]; + if ( + lastToken && + lastToken.role_id === token.role_id && + lastToken.related_role_id === token.related_role_id + ) { + continue; + } + + acc.push({ + name: token.role?.name || "", + alignment: token.alignment, + related: token.related_role?.name || "", + role_id: token.role_id, + related_role_id: token.related_role_id, + }); + } + } + + return acc; + }, + [] as { + name: string; + alignment: Alignment; + related: string; + role_id: string | null; + related_role_id: string | null; + }[] + ); + + const relatedGame = related_games?.find((g) => g!.user_id === id); + + try { + if (!relatedGame) { + await prisma.game.create({ + data: { + ...game, + community: undefined, + associated_script: undefined, + parent_game: undefined, + child_games: undefined, + date: new Date(game.date), + user_id: id, + player_characters: { + create: [...player_characters], + }, + demon_bluffs: { + create: [...game.demon_bluffs], + }, + fabled: { + create: [...game.fabled], + }, + // map the already created grimoires to the new game + grimoire: { + connect: game.grimoire.map((g) => ({ id: g.id })), + }, + parent_game_id: game.parent_game_id || game.id, + waiting_for_confirmation: true, + is_storyteller: false, + }, + }); + } else { + await prisma.game.update({ + where: { + id: relatedGame.id, + }, + data: { + date: new Date(game.date), + script: game.script, + script_id: game.script_id, + location_type: game.location_type, + location: game.location, + community_name: game.community_name, + community_id: game.community_id, + player_count: game.player_count, + traveler_count: game.traveler_count, + storyteller: game.storyteller, + co_storytellers: game.co_storytellers, + win_v2: game.win_v2, + demon_bluffs: { + deleteMany: relatedGame.demon_bluffs.map((g) => ({ id: g.id })), + create: game.demon_bluffs.map((g) => ({ + ...g, + id: undefined, + game_id: undefined, + })), + }, + fabled: { + deleteMany: relatedGame.fabled.map((g) => ({ id: g.id })), + create: game.fabled.map((g) => ({ + ...g, + id: undefined, + game_id: undefined, + })), + }, + player_characters: { + deleteMany: relatedGame.player_characters.map((g) => ({ + id: g.id, + })), + create: player_characters, + }, + grimoire: { + connect: game.grimoire.map((g) => ({ id: g.id })), + }, + }, + }); + } + } catch (err: any) { + const messageLines = err.message.split("\n"); + const message = + messageLines[messageLines.length - 1].length > 0 + ? messageLines[messageLines.length - 1] + : err.message; + // get the name from the game.grimoire + const taggedPlayer = + game.grimoire.flatMap((g) => g.tokens).find((t) => t.player_id === id) + ?.player_name || "Unknown"; + + console.error(`Error saving for ${taggedPlayer}: ${message}`); + + throw createError({ + status: 500, + statusMessage: `Error saving for ${taggedPlayer}: ${message}`, + }); + } + } + + const storytellers = [game.storyteller, ...game.co_storytellers]; + + for (const storyteller of storytellers) { + if (storyteller?.includes("@")) { + // Verify that it's a friend + const friend = await prisma.userSettings.findUnique({ + where: { + username: storyteller.replace("@", ""), + friends: { + some: { + user_id: user.id, + }, + }, + }, + }); + + if (friend !== null) { + const childGame = game.child_games?.find( + (g) => g.user_id === friend.user_id + ); + + if (!childGame) { + await prisma.game.create({ + data: { + ...game, + community: undefined, + associated_script: undefined, + parent_game: undefined, + child_games: undefined, + is_storyteller: true, + date: new Date(game.date), + user_id: friend.user_id, + player_characters: {}, + demon_bluffs: { + create: [...game.demon_bluffs], + }, + fabled: { + create: [...game.fabled], + }, + notes: "", + grimoire: { + connect: game.grimoire.map((g) => ({ id: g.id })), + }, + parent_game_id: game.id, + waiting_for_confirmation: true, + tags: [], + }, + }); + } else { + await prisma.game.update({ + where: { + id: childGame.id, + }, + data: { + date: new Date(game.date), + script: game.script, + script_id: game.script_id, + location_type: game.location_type, + location: game.location, + community_name: game.community_name, + community_id: game.community_id, + player_count: game.player_count, + traveler_count: game.traveler_count, + storyteller: game.storyteller, + co_storytellers: game.co_storytellers, + win_v2: game.win_v2, + demon_bluffs: { + deleteMany: childGame.demon_bluffs.map((g) => ({ id: g.id })), + create: game.demon_bluffs.map((g) => ({ + ...g, + id: undefined, + game_id: undefined, + })), + }, + fabled: { + deleteMany: childGame.fabled.map((g) => ({ id: g.id })), + create: game.fabled.map((g) => ({ + ...g, + id: undefined, + game_id: undefined, + })), + }, + grimoire: { + connect: game.grimoire.map((g) => ({ id: g.id })), + }, + }, + }); + } + } + } + } + } +}); diff --git a/server/api/user/[username]/v2/games.get.ts b/server/api/user/[username]/v2/games.get.ts new file mode 100644 index 00000000..dd76a19d --- /dev/null +++ b/server/api/user/[username]/v2/games.get.ts @@ -0,0 +1,34 @@ +import { PrismaClient } from "@prisma/client"; +import { User } from "@supabase/supabase-js"; +import { fetchGamesV2 } from "~/server/utils/fetchGames"; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (handler) => { + const me: User | null = handler.context.user; + const username = handler.context.params?.username as string; + + const user = await prisma.userSettings.findUnique({ + where: { + username, + }, + select: { + user_id: true, + }, + }); + + if (!user) { + console.error(`User not found: ${username}`); + throw createError({ + status: 404, + statusMessage: "Not Found", + }); + } + + return fetchGamesV2( + { + user_id: user.user_id, + }, + me + ); +}); diff --git a/server/utils/fetchGames.ts b/server/utils/fetchGames.ts index ac0aacbc..87b73a5f 100644 --- a/server/utils/fetchGames.ts +++ b/server/utils/fetchGames.ts @@ -1,6 +1,7 @@ import type { User } from "@supabase/supabase-js"; -import { PrismaClient, PrivacySetting } from "@prisma/client"; +import { Game, PrismaClient, PrivacySetting } from "@prisma/client"; import { anonymizeGame, GameRecord } from "~/server/utils/anonymizeGame"; +import { GameRecordV2 } from "~/shared/GameRecordV2"; const prisma = new PrismaClient(); @@ -538,3 +539,581 @@ export async function fetchGame( return anonymizeGame(game as GameRecord, me, isFriend); } + +export async function fetchGamesV2( + params: Partial<{ + user_id: string; + community_slug: string; + }>, + me: User | null +): Promise { + const user_id = params.user_id; + const slug = params.community_slug; + + const games = await prisma.gameV2.findMany({ + where: { + deleted: false, + user_id, + details: slug ? { community: { slug } } : undefined, + OR: [ + // PUBLIC PROFILE + // PUBLIC GAME + { + user: { + privacy: PrivacySetting.PUBLIC, + }, + details: { + privacy: PrivacySetting.PUBLIC, + }, + }, + // PRIVATE GAME + // This is an indirect lookup for a given user, so we need + // to make sure that the user can see the games. + { + user: { + privacy: PrivacySetting.PUBLIC, + OR: [ + { + user_id: me?.id || "", + }, + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + ], + }, + details: { + privacy: PrivacySetting.PRIVATE, + }, + }, + // FRIENDS ONLY GAME + { + user: { + privacy: PrivacySetting.PUBLIC, + OR: [ + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + { + user_id: me?.id || "", + }, + ], + }, + details: { + privacy: PrivacySetting.FRIENDS_ONLY, + }, + }, + // PRIVATE PROFILE + // PUBLIC GAME + // No filtering done here, because the game is public. + // We'll need to anonymize the user's profile, though. + { + user: { + privacy: PrivacySetting.PRIVATE, + }, + details: { + privacy: PrivacySetting.PUBLIC, + }, + }, + // PRIVATE GAME + // This is an indirect lookup for a given user, so we need + // to make sure that the user can see the games. + { + user: { + privacy: PrivacySetting.PRIVATE, + OR: [ + { + user_id: me?.id || "", + }, + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + ], + }, + details: { + privacy: PrivacySetting.PRIVATE, + }, + }, + // FRIENDS ONLY GAME + { + user: { + privacy: PrivacySetting.PRIVATE, + OR: [ + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + { + user_id: me?.id || "", + }, + ], + }, + details: { + privacy: PrivacySetting.FRIENDS_ONLY, + }, + }, + // FRIENDS ONLY PROFILE + // PUBLIC GAME + // We're just treating this as a friends only game, because + // we don't want to show the user's profile. + { + user: { + privacy: PrivacySetting.FRIENDS_ONLY, + OR: [ + { + user_id: me?.id || "", + }, + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + ], + }, + details: { + privacy: PrivacySetting.PUBLIC, + }, + }, + // PRIVATE GAME + // Direct links are allowed, so we aren't performing any checks on the user + { + user: { + privacy: PrivacySetting.FRIENDS_ONLY, + OR: [ + { + user_id: me?.id || "", + }, + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + ], + }, + details: { + privacy: PrivacySetting.PRIVATE, + }, + }, + // FRIENDS ONLY GAME + { + user: { + privacy: PrivacySetting.FRIENDS_ONLY, + OR: [ + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + { + user_id: me?.id || "", + }, + ], + }, + details: { + privacy: PrivacySetting.FRIENDS_ONLY, + }, + }, + ], + }, + include: { + user: { + select: { + privacy: true, + username: true, + }, + }, + characters: { + include: { + role: { + select: { + token_url: true, + type: true, + initial_alignment: true, + }, + }, + related_role: { + select: { + token_url: true, + }, + }, + }, + }, + details: { + include: { + storytellers: { + select: { + username: true, + display_name: true, + }, + }, + grimoire: { + include: { + tokens: { + include: { + role: true, + related_role: true, + reminders: true, + player: { + select: { + display_name: true, + }, + }, + }, + }, + }, + }, + demon_bluffs: { + include: { + role: { + select: { + token_url: true, + type: true, + }, + }, + }, + }, + fabled: { + include: { + role: { + select: { + token_url: true, + }, + }, + }, + }, + community: { + select: { + id: true, + name: true, + slug: true, + icon: true, + }, + }, + associated_script: { + select: { + version: true, + script_id: true, + is_custom_script: true, + logo: true, + }, + }, + }, + }, + }, + orderBy: [ + { + details: { + date: "desc", + }, + }, + { + created_at: "desc", + }, + { + id: "desc", + }, + ], + }); + + return games; +} + +export async function fetchGameV2( + id: string, + me: User | null, + my_game: boolean = false +): Promise { + const game = await prisma.gameV2.findUnique({ + where: { + id, + deleted: false, + user_id: my_game ? me?.id || "" : undefined, + OR: [ + // PUBLIC PROFILE + // PUBLIC GAME + { + user: { + privacy: PrivacySetting.PUBLIC, + }, + details: { + privacy: PrivacySetting.PUBLIC, + }, + }, + // PRIVATE GAME + // This is an indirect lookup for a given user, so we need + // to make sure that the user can see the games. + { + user: { + privacy: PrivacySetting.PUBLIC, + OR: [ + { + user_id: me?.id || "", + }, + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + ], + }, + details: { + privacy: PrivacySetting.PRIVATE, + }, + }, + // FRIENDS ONLY GAME + { + user: { + privacy: PrivacySetting.PUBLIC, + OR: [ + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + { + user_id: me?.id || "", + }, + ], + }, + details: { + privacy: PrivacySetting.FRIENDS_ONLY, + }, + }, + // PRIVATE PROFILE + // PUBLIC GAME + // No filtering done here, because the game is public. + // We'll need to anonymize the user's profile, though. + { + user: { + privacy: PrivacySetting.PRIVATE, + }, + details: { + privacy: PrivacySetting.PUBLIC, + }, + }, + // PRIVATE GAME + // This is an indirect lookup for a given user, so we need + // to make sure that the user can see the games. + { + user: { + privacy: PrivacySetting.PRIVATE, + OR: [ + { + user_id: me?.id || "", + }, + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + ], + }, + details: { + privacy: PrivacySetting.PRIVATE, + }, + }, + // FRIENDS ONLY GAME + { + user: { + privacy: PrivacySetting.PRIVATE, + OR: [ + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + { + user_id: me?.id || "", + }, + ], + }, + details: { + privacy: PrivacySetting.FRIENDS_ONLY, + }, + }, + // FRIENDS ONLY PROFILE + // PUBLIC GAME + // We're just treating this as a friends only game, because + // we don't want to show the user's profile. + { + user: { + privacy: PrivacySetting.FRIENDS_ONLY, + OR: [ + { + user_id: me?.id || "", + }, + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + ], + }, + details: { + privacy: PrivacySetting.PUBLIC, + }, + }, + // PRIVATE GAME + // Direct links are allowed, so we aren't performing any checks on the user + { + user: { + privacy: PrivacySetting.FRIENDS_ONLY, + OR: [ + { + user_id: me?.id || "", + }, + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + ], + }, + details: { + privacy: PrivacySetting.PRIVATE, + }, + }, + // FRIENDS ONLY GAME + { + user: { + privacy: PrivacySetting.FRIENDS_ONLY, + OR: [ + { + friends: { + some: { + user_id: me?.id || "", + }, + }, + }, + { + user_id: me?.id || "", + }, + ], + }, + details: { + privacy: PrivacySetting.FRIENDS_ONLY, + }, + }, + ], + }, + include: { + user: { + select: { + privacy: true, + username: true, + }, + }, + characters: { + include: { + role: { + select: { + token_url: true, + type: true, + initial_alignment: true, + }, + }, + related_role: { + select: { + token_url: true, + }, + }, + }, + }, + details: { + include: { + storytellers: { + select: { + username: true, + display_name: true, + }, + }, + grimoire: { + include: { + tokens: { + include: { + role: true, + related_role: true, + reminders: true, + player: { + select: { + display_name: true, + }, + }, + }, + }, + }, + }, + demon_bluffs: { + include: { + role: { + select: { + token_url: true, + type: true, + }, + }, + }, + }, + fabled: { + include: { + role: { + select: { + token_url: true, + }, + }, + }, + }, + community: { + select: { + id: true, + name: true, + slug: true, + icon: true, + }, + }, + associated_script: { + select: { + version: true, + script_id: true, + is_custom_script: true, + logo: true, + }, + }, + }, + }, + }, + }); + + return game; +} diff --git a/shared/GameRecordV2.ts b/shared/GameRecordV2.ts new file mode 100644 index 00000000..5c6dfd97 --- /dev/null +++ b/shared/GameRecordV2.ts @@ -0,0 +1,77 @@ +import type { + GameV2, + GameDetails, + GrimoireV2, + TokenV2, + Character, + DemonBluff, + Fabled, + ReminderToken, + Alignment, +} from "@prisma/client"; + +export type GameSummary = { + date: string; + script: string; + script_logo: string | null; + display_name: string; +}; + +export type GameRecordV2 = GameV2 & { + user: { + username: string; + privacy: PrivacySetting; + }; + characters: (Character & { + role: { + token_url: string; + type: string; + initial_alignment: Alignment; + } | null; + related_role: { token_url: string } | null; + })[]; + details: GameDetails & { + storytellers: { + username: string; + display_name: string; + }[]; + grimoire: (GrimoireV2 & { + tokens: (TokenV2 & { + role: { + token_url: string; + type: string; + initial_alignment: "GOOD" | "EVIL" | "NEUTRAL"; + name: string; + } | null; + related_role: { token_url: string } | null; + reminders: ReminderToken[]; + player: { + username: string; + display_name: string; + } | null; + })[]; + })[]; + demon_bluffs: (DemonBluff & { + role: { + token_url: string; + type: string; + } | null; + })[]; + fabled: (Fabled & { + role: { + token_url: string; + } | null; + })[]; + community: { + id: number; + name: string; + slug: string; + } | null; + associated_script: { + version: string; + script_id: string; + is_custom_script: boolean; + logo: string | null; + } | null; + }; +};