From fd55f4fb7ff25a9c4c789aefb5f40d048880f814 Mon Sep 17 00:00:00 2001 From: seavor Date: Mon, 13 Apr 2026 15:03:57 -0500 Subject: [PATCH] migrate to Protobuf ES --- webclient/.gitignore | 1 + webclient/buf.gen.yaml | 8 + webclient/package-lock.json | 354 ++++++++++- webclient/package.json | 7 +- webclient/prebuild.js | 18 - .../src/api/AuthenticationService.spec.ts | 12 +- webclient/src/api/AuthenticationService.tsx | 4 +- webclient/src/setupTests.ts | 8 - webclient/src/store/common/SortUtil.spec.ts | 10 +- .../store/rooms/__mocks__/rooms-fixtures.ts | 5 +- .../store/server/__mocks__/server-fixtures.ts | 5 +- webclient/src/types/game.ts | 597 +++++------------- webclient/src/types/message.ts | 10 +- webclient/src/types/replay.ts | 19 +- webclient/src/types/room.ts | 25 +- webclient/src/types/session.ts | 8 +- webclient/src/types/user.ts | 26 +- webclient/src/websocket/WebClient.spec.ts | 6 +- webclient/src/websocket/WebClient.ts | 2 + .../websocket/__mocks__/callbackHelpers.ts | 4 +- webclient/src/websocket/__mocks__/helpers.ts | 47 +- .../__mocks__/sessionCommandMocks.ts | 32 - .../src/websocket/commands/admin/adjustMod.ts | 4 +- .../commands/admin/adminCommands.spec.ts | 19 +- .../websocket/commands/admin/reloadConfig.ts | 4 +- .../commands/admin/shutdownServer.ts | 4 +- .../commands/admin/updateServerMessage.ts | 4 +- .../src/websocket/commands/game/attachCard.ts | 4 +- .../commands/game/changeZoneProperties.ts | 4 +- .../src/websocket/commands/game/concede.ts | 4 +- .../websocket/commands/game/createArrow.ts | 4 +- .../websocket/commands/game/createCounter.ts | 4 +- .../websocket/commands/game/createToken.ts | 4 +- .../src/websocket/commands/game/deckSelect.ts | 4 +- .../src/websocket/commands/game/delCounter.ts | 4 +- .../websocket/commands/game/deleteArrow.ts | 4 +- .../src/websocket/commands/game/drawCards.ts | 4 +- .../src/websocket/commands/game/dumpZone.ts | 4 +- .../src/websocket/commands/game/flipCard.ts | 4 +- .../commands/game/gameCommands.spec.ts | 198 ++++-- .../src/websocket/commands/game/gameSay.ts | 4 +- .../websocket/commands/game/incCardCounter.ts | 4 +- .../src/websocket/commands/game/incCounter.ts | 4 +- .../src/websocket/commands/game/judge.ts | 10 +- .../websocket/commands/game/kickFromGame.ts | 4 +- .../src/websocket/commands/game/leaveGame.ts | 4 +- .../src/websocket/commands/game/moveCard.ts | 4 +- .../src/websocket/commands/game/mulligan.ts | 4 +- .../src/websocket/commands/game/nextTurn.ts | 4 +- .../src/websocket/commands/game/readyStart.ts | 4 +- .../websocket/commands/game/revealCards.ts | 4 +- .../websocket/commands/game/reverseTurn.ts | 4 +- .../websocket/commands/game/setActivePhase.ts | 4 +- .../websocket/commands/game/setCardAttr.ts | 4 +- .../websocket/commands/game/setCardCounter.ts | 4 +- .../src/websocket/commands/game/setCounter.ts | 4 +- .../commands/game/setSideboardLock.ts | 4 +- .../commands/game/setSideboardPlan.ts | 4 +- .../src/websocket/commands/game/shuffle.ts | 4 +- .../src/websocket/commands/game/unconcede.ts | 4 +- .../src/websocket/commands/game/undoDraw.ts | 4 +- .../commands/moderator/banFromServer.ts | 6 +- .../commands/moderator/forceActivateUser.ts | 7 +- .../commands/moderator/getAdminNotes.ts | 7 +- .../commands/moderator/getBanHistory.ts | 7 +- .../commands/moderator/getWarnHistory.ts | 7 +- .../commands/moderator/getWarnList.ts | 7 +- .../commands/moderator/grantReplayAccess.ts | 6 +- .../moderator/moderatorCommands.spec.ts | 64 +- .../commands/moderator/updateAdminNotes.ts | 6 +- .../commands/moderator/viewLogHistory.ts | 7 +- .../websocket/commands/moderator/warnUser.ts | 5 +- .../src/websocket/commands/room/createGame.ts | 4 +- .../src/websocket/commands/room/joinGame.ts | 4 +- .../src/websocket/commands/room/leaveRoom.ts | 4 +- .../commands/room/roomCommands.spec.ts | 19 +- .../src/websocket/commands/room/roomSay.ts | 4 +- .../websocket/commands/session/accountEdit.ts | 5 +- .../commands/session/accountImage.ts | 4 +- .../commands/session/accountPassword.ts | 5 +- .../websocket/commands/session/activate.ts | 10 +- .../websocket/commands/session/addToList.ts | 4 +- .../src/websocket/commands/session/deckDel.ts | 4 +- .../websocket/commands/session/deckDelDir.ts | 4 +- .../websocket/commands/session/deckList.ts | 7 +- .../websocket/commands/session/deckNewDir.ts | 4 +- .../websocket/commands/session/deckUpload.ts | 7 +- .../session/forgotPasswordChallenge.ts | 8 +- .../commands/session/forgotPasswordRequest.ts | 11 +- .../commands/session/forgotPasswordReset.ts | 18 +- .../commands/session/getGamesOfUser.ts | 7 +- .../websocket/commands/session/getUserInfo.ts | 7 +- .../websocket/commands/session/joinRoom.ts | 7 +- .../websocket/commands/session/listRooms.ts | 4 +- .../websocket/commands/session/listUsers.ts | 7 +- .../src/websocket/commands/session/login.ts | 41 +- .../src/websocket/commands/session/message.ts | 4 +- .../src/websocket/commands/session/ping.ts | 4 +- .../websocket/commands/session/register.ts | 40 +- .../commands/session/removeFromList.ts | 4 +- .../commands/session/replayDeleteMatch.ts | 4 +- .../commands/session/replayGetCode.ts | 7 +- .../websocket/commands/session/replayList.ts | 7 +- .../commands/session/replayModifyMatch.ts | 4 +- .../commands/session/replaySubmitCode.ts | 4 +- .../commands/session/requestPasswordSalt.ts | 15 +- .../session/sessionCommands-complex.spec.ts | 125 ++-- .../session/sessionCommands-simple.spec.ts | 162 +++-- .../events/common/commonEvents.spec.ts | 2 +- .../src/websocket/events/common/index.ts | 4 +- webclient/src/websocket/events/game/index.ts | 95 ++- webclient/src/websocket/events/room/index.ts | 23 +- .../src/websocket/events/room/interfaces.ts | 33 +- .../src/websocket/events/room/joinRoom.ts | 2 +- .../src/websocket/events/room/leaveRoom.ts | 2 +- .../src/websocket/events/room/listGames.ts | 2 +- .../websocket/events/room/removeMessages.ts | 2 +- .../websocket/events/room/roomEvents.spec.ts | 2 +- .../src/websocket/events/room/roomSay.ts | 2 +- .../events/session/connectionClosed.ts | 19 +- .../src/websocket/events/session/index.ts | 50 +- .../websocket/events/session/interfaces.ts | 119 +--- .../events/session/sessionEvents.spec.ts | 50 +- .../persistence/SessionPersistence.ts | 12 +- .../websocket/services/BackendService.spec.ts | 68 +- .../src/websocket/services/BackendService.ts | 97 ++- .../services/ProtoController.spec.ts | 40 -- .../src/websocket/services/ProtoController.ts | 24 - .../services/ProtobufService.spec.ts | 292 +++++---- .../src/websocket/services/ProtobufService.ts | 122 ++-- .../websocket/utils/passwordHasher.spec.ts | 11 +- .../src/websocket/utils/passwordHasher.ts | 4 +- webclient/tsconfig.json | 2 +- 133 files changed, 1745 insertions(+), 1621 deletions(-) create mode 100644 webclient/buf.gen.yaml delete mode 100644 webclient/src/websocket/services/ProtoController.spec.ts delete mode 100644 webclient/src/websocket/services/ProtoController.ts diff --git a/webclient/.gitignore b/webclient/.gitignore index 2b30e2c8d..d0eec90c8 100644 --- a/webclient/.gitignore +++ b/webclient/.gitignore @@ -8,6 +8,7 @@ # generated ./src files /src/proto-files.json /src/server-props.json +/src/generated/ # testing /coverage diff --git a/webclient/buf.gen.yaml b/webclient/buf.gen.yaml new file mode 100644 index 000000000..eba5ea4ad --- /dev/null +++ b/webclient/buf.gen.yaml @@ -0,0 +1,8 @@ +version: v2 +inputs: + - directory: ../libcockatrice_protocol/libcockatrice/protocol/pb +plugins: + - local: protoc-gen-es + out: src/generated/proto + opt: + - target=ts diff --git a/webclient/package-lock.json b/webclient/package-lock.json index af3f74e50..dd410fe16 100644 --- a/webclient/package-lock.json +++ b/webclient/package-lock.json @@ -8,6 +8,7 @@ "name": "webclient", "version": "1.0.0", "dependencies": { + "@bufbuild/protobuf": "^2.11.0", "@emotion/react": "^11.8.2", "@emotion/styled": "^11.8.1", "@mui/icons-material": "^5.5.1", @@ -39,6 +40,8 @@ "rxjs": "^7.5.4" }, "devDependencies": { + "@bufbuild/buf": "^1.67.0", + "@bufbuild/protoc-gen-es": "^2.11.0", "@mui/types": "^7.1.3", "@testing-library/jest-dom": "^6.4.0", "@testing-library/react": "^13.4.0", @@ -433,6 +436,207 @@ "dev": true, "license": "MIT" }, + "node_modules/@bufbuild/buf": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.67.0.tgz", + "integrity": "sha512-BLfgGmNFiHM79PcaafFNiP/+xxbdyFp1neDDdJd6R0tu7McO+WgJHM6vyNYRm7vXOSgO1uUPE4X3YFdBgcWk2Q==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "buf": "bin/buf", + "protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking", + "protoc-gen-buf-lint": "bin/protoc-gen-buf-lint" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@bufbuild/buf-darwin-arm64": "1.67.0", + "@bufbuild/buf-darwin-x64": "1.67.0", + "@bufbuild/buf-linux-aarch64": "1.67.0", + "@bufbuild/buf-linux-armv7": "1.67.0", + "@bufbuild/buf-linux-x64": "1.67.0", + "@bufbuild/buf-win32-arm64": "1.67.0", + "@bufbuild/buf-win32-x64": "1.67.0" + } + }, + "node_modules/@bufbuild/buf-darwin-arm64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.67.0.tgz", + "integrity": "sha512-9h/1E2FNCSIt9m4wriGiXt8gHrg8VBOOpmUPVr68axZxb17krPQrIZBPsx05yNpbyvSrPj26/jO2aoqpZsG1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-darwin-x64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.67.0.tgz", + "integrity": "sha512-9kNu0JBR+TQvxCD6NBooy03g8sLNZGEd0umkWHzdO/05HuV/J6GecMGx1kJ2MYlZQHM4/MljfIuYQUblP1nP4A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-linux-aarch64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.67.0.tgz", + "integrity": "sha512-hlA20Oot20nW/9CzPBMPPPMfUarKvzqni+Njgrw8T43IFoQWQv8iIRoWWOgOQTGCm4PmjYwiojzEHOEaaKrzTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-linux-armv7": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-armv7/-/buf-linux-armv7-1.67.0.tgz", + "integrity": "sha512-hO9FEEtloITNaxW89rzKUjAsgnX1+rth7IZbK0Z+ohatXdanYg7Kv66yWffytaYf2iHltTbY6W/H4C3x0Uimbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-linux-x64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.67.0.tgz", + "integrity": "sha512-KBOWZ0NbhJSfXLM3JEX2AEs32jyHvTKD7wkIYudqOTxPUqwM1MXUg7m2Xw5nP1pcKH4RKS5HFijPMeOW/XUQ8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-win32-arm64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.67.0.tgz", + "integrity": "sha512-ARGPwOv0lkUp3FU7bUMpYzqoJInx2qkk1ECBEC9XZMnRKmhCbyzmBoBKChBBJhEyDFdzPivhjg//zk5AlQ3bFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/buf-win32-x64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.67.0.tgz", + "integrity": "sha512-x9fkxEbjb2U4petBbESvNx+sfSQJONJxKOQzPfEKALksqRlvh7ktoHrYbygErnRZBSTNgrXzAqFI1GxMGEGSLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@bufbuild/protoc-gen-es": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-2.11.0.tgz", + "integrity": "sha512-VzQuwEQDXipbZ1soWUuAWm1Z0C3B/IDWGeysnbX6ogJ6As91C2mdvAND/ekQ4YIWgen4d5nqLfIBOWLqCCjYUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "2.11.0", + "@bufbuild/protoplugin": "2.11.0" + }, + "bin": { + "protoc-gen-es": "bin/protoc-gen-es" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@bufbuild/protobuf": "2.11.0" + }, + "peerDependenciesMeta": { + "@bufbuild/protobuf": { + "optional": true + } + } + }, + "node_modules/@bufbuild/protoplugin": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.11.0.tgz", + "integrity": "sha512-lyZVNFUHArIOt4W0+dwYBe5GBwbKzbOy8ObaloEqsw9Mmiwv2O48TwddDoHN4itylC+BaEGqFdI1W8WQt2vWJQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "2.11.0", + "@typescript/vfs": "^1.6.2", + "typescript": "5.4.5" + } + }, + "node_modules/@bufbuild/protoplugin/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -2543,6 +2747,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript/vfs": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.4.tgz", + "integrity": "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3" + }, + "peerDependencies": { + "typescript": "*" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -3254,11 +3471,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -5369,9 +5587,10 @@ "license": "MIT" }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.8", @@ -7649,6 +7868,104 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@bufbuild/buf": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.67.0.tgz", + "integrity": "sha512-BLfgGmNFiHM79PcaafFNiP/+xxbdyFp1neDDdJd6R0tu7McO+WgJHM6vyNYRm7vXOSgO1uUPE4X3YFdBgcWk2Q==", + "dev": true, + "requires": { + "@bufbuild/buf-darwin-arm64": "1.67.0", + "@bufbuild/buf-darwin-x64": "1.67.0", + "@bufbuild/buf-linux-aarch64": "1.67.0", + "@bufbuild/buf-linux-armv7": "1.67.0", + "@bufbuild/buf-linux-x64": "1.67.0", + "@bufbuild/buf-win32-arm64": "1.67.0", + "@bufbuild/buf-win32-x64": "1.67.0" + } + }, + "@bufbuild/buf-darwin-arm64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.67.0.tgz", + "integrity": "sha512-9h/1E2FNCSIt9m4wriGiXt8gHrg8VBOOpmUPVr68axZxb17krPQrIZBPsx05yNpbyvSrPj26/jO2aoqpZsG1vw==", + "dev": true, + "optional": true + }, + "@bufbuild/buf-darwin-x64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.67.0.tgz", + "integrity": "sha512-9kNu0JBR+TQvxCD6NBooy03g8sLNZGEd0umkWHzdO/05HuV/J6GecMGx1kJ2MYlZQHM4/MljfIuYQUblP1nP4A==", + "dev": true, + "optional": true + }, + "@bufbuild/buf-linux-aarch64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.67.0.tgz", + "integrity": "sha512-hlA20Oot20nW/9CzPBMPPPMfUarKvzqni+Njgrw8T43IFoQWQv8iIRoWWOgOQTGCm4PmjYwiojzEHOEaaKrzTg==", + "dev": true, + "optional": true + }, + "@bufbuild/buf-linux-armv7": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-armv7/-/buf-linux-armv7-1.67.0.tgz", + "integrity": "sha512-hO9FEEtloITNaxW89rzKUjAsgnX1+rth7IZbK0Z+ohatXdanYg7Kv66yWffytaYf2iHltTbY6W/H4C3x0Uimbg==", + "dev": true, + "optional": true + }, + "@bufbuild/buf-linux-x64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.67.0.tgz", + "integrity": "sha512-KBOWZ0NbhJSfXLM3JEX2AEs32jyHvTKD7wkIYudqOTxPUqwM1MXUg7m2Xw5nP1pcKH4RKS5HFijPMeOW/XUQ8Q==", + "dev": true, + "optional": true + }, + "@bufbuild/buf-win32-arm64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.67.0.tgz", + "integrity": "sha512-ARGPwOv0lkUp3FU7bUMpYzqoJInx2qkk1ECBEC9XZMnRKmhCbyzmBoBKChBBJhEyDFdzPivhjg//zk5AlQ3bFA==", + "dev": true, + "optional": true + }, + "@bufbuild/buf-win32-x64": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.67.0.tgz", + "integrity": "sha512-x9fkxEbjb2U4petBbESvNx+sfSQJONJxKOQzPfEKALksqRlvh7ktoHrYbygErnRZBSTNgrXzAqFI1GxMGEGSLQ==", + "dev": true, + "optional": true + }, + "@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==" + }, + "@bufbuild/protoc-gen-es": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-2.11.0.tgz", + "integrity": "sha512-VzQuwEQDXipbZ1soWUuAWm1Z0C3B/IDWGeysnbX6ogJ6As91C2mdvAND/ekQ4YIWgen4d5nqLfIBOWLqCCjYUA==", + "dev": true, + "requires": { + "@bufbuild/protobuf": "2.11.0", + "@bufbuild/protoplugin": "2.11.0" + } + }, + "@bufbuild/protoplugin": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-2.11.0.tgz", + "integrity": "sha512-lyZVNFUHArIOt4W0+dwYBe5GBwbKzbOy8ObaloEqsw9Mmiwv2O48TwddDoHN4itylC+BaEGqFdI1W8WQt2vWJQ==", + "dev": true, + "requires": { + "@bufbuild/protobuf": "2.11.0", + "@typescript/vfs": "^1.6.2", + "typescript": "5.4.5" + }, + "dependencies": { + "typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true + } + } + }, "@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -8911,6 +9228,15 @@ "eslint-visitor-keys": "^3.3.0" } }, + "@typescript/vfs": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.4.tgz", + "integrity": "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ==", + "dev": true, + "requires": { + "debug": "^4.4.3" + } + }, "@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -9403,11 +9729,11 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "decimal.js": { @@ -10927,9 +11253,9 @@ } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "nanoid": { "version": "3.3.8", diff --git a/webclient/package.json b/webclient/package.json index 9c24e60a4..527ebfa7c 100644 --- a/webclient/package.json +++ b/webclient/package.json @@ -14,9 +14,11 @@ "lint:fix": "eslint src/**/*.{ts,tsx} --fix", "golden": "npm run lint && npm run test", "prepare": "cd .. && husky install", - "translate": "node prebuild.js -i18nOnly" + "translate": "node prebuild.js -i18nOnly", + "proto:generate": "npx buf generate" }, "dependencies": { + "@bufbuild/protobuf": "^2.11.0", "@emotion/react": "^11.8.2", "@emotion/styled": "^11.8.1", "@mui/icons-material": "^5.5.1", @@ -32,7 +34,6 @@ "intl-messageformat": "^10.2.1", "lodash": "^4.17.21", "prop-types": "^15.8.1", - "protobufjs": "^7.2.4", "react": "^18.2.0", "react-dom": "^18.2.0", "react-final-form": "^6.5.8", @@ -48,6 +49,8 @@ "rxjs": "^7.5.4" }, "devDependencies": { + "@bufbuild/buf": "^1.67.0", + "@bufbuild/protoc-gen-es": "^2.11.0", "@mui/types": "^7.1.3", "@testing-library/jest-dom": "^6.4.0", "@testing-library/react": "^13.4.0", diff --git a/webclient/prebuild.js b/webclient/prebuild.js index 3b420f32c..7f0a585e1 100644 --- a/webclient/prebuild.js +++ b/webclient/prebuild.js @@ -6,13 +6,10 @@ const exec = util.promisify(require('child_process').exec); const ROOT_DIR = './src'; const PUBLIC_DIR = './public'; -const protoFilesDir = `${PUBLIC_DIR}/pb`; const i18nDefaultFile = `${ROOT_DIR}/i18n-default.json`; const serverPropsFile = `${ROOT_DIR}/server-props.json`; -const masterProtoFile = `${ROOT_DIR}/proto-files.json`; const sharedFiles = [ - ['../libcockatrice_protocol/libcockatrice/protocol/pb', protoFilesDir], ['../cockatrice/resources/countries', `${ROOT_DIR}/images/countries`], ]; @@ -26,10 +23,8 @@ const i18nOnly = process.argv.indexOf('-i18nOnly') > -1; return; } - // make sure these files finish copying before master file is created await copySharedFiles(); - await createMasterProtoFile(); await createServerProps(); await createI18NDefault(); })(); @@ -43,19 +38,6 @@ async function copySharedFiles() { } } -async function createMasterProtoFile() { - try { - fse.readdir(protoFilesDir, (err, files) => { - if (err) throw err; - - fse.outputFile(masterProtoFile, JSON.stringify(files.filter(file => /\.proto$/.test(file)))); - }); - } catch (e) { - console.error(e); - process.exitCode = 1; - } -} - async function createServerProps() { try { fse.outputFile(serverPropsFile, JSON.stringify({ diff --git a/webclient/src/api/AuthenticationService.spec.ts b/webclient/src/api/AuthenticationService.spec.ts index 460c8af0f..1ba4cd252 100644 --- a/webclient/src/api/AuthenticationService.spec.ts +++ b/webclient/src/api/AuthenticationService.spec.ts @@ -8,15 +8,9 @@ vi.mock('websocket', () => ({ }, })); -vi.mock('websocket/services/ProtoController', () => ({ - ProtoController: { - root: { - ServerInfo_User: { - UserLevelFlag: { - IsModerator: 4, - }, - }, - }, +vi.mock('generated/proto/serverinfo_user_pb', () => ({ + ServerInfo_User_UserLevelFlag: { + IsModerator: 4, }, })); diff --git a/webclient/src/api/AuthenticationService.tsx b/webclient/src/api/AuthenticationService.tsx index 7b3a46988..c5128fcbd 100644 --- a/webclient/src/api/AuthenticationService.tsx +++ b/webclient/src/api/AuthenticationService.tsx @@ -1,6 +1,6 @@ import { StatusEnum, User, WebSocketConnectReason, WebSocketConnectOptions } from 'types'; import { SessionCommands, webClient } from 'websocket'; -import { ProtoController } from 'websocket/services/ProtoController'; +import { ServerInfo_User_UserLevelFlag } from 'generated/proto/serverinfo_user_pb'; export class AuthenticationService { static login(options: WebSocketConnectOptions): void { @@ -40,7 +40,7 @@ export class AuthenticationService { } static isModerator(user: User): boolean { - const moderatorLevel = ProtoController.root.ServerInfo_User.UserLevelFlag.IsModerator; + const moderatorLevel = ServerInfo_User_UserLevelFlag.IsModerator; // @TODO tell cockatrice not to do this so shittily return (user.userLevel & moderatorLevel) === moderatorLevel; } diff --git a/webclient/src/setupTests.ts b/webclient/src/setupTests.ts index bfb001b2a..5b9a2b986 100644 --- a/webclient/src/setupTests.ts +++ b/webclient/src/setupTests.ts @@ -1,10 +1,2 @@ -import protobuf from 'protobufjs'; - // ensure jest-dom is always available during testing to cut down on boilerplate import '@testing-library/jest-dom/vitest'; - -class MockProtobufRoot { - load() {} -} - -(protobuf as any).Root = MockProtobufRoot; diff --git a/webclient/src/store/common/SortUtil.spec.ts b/webclient/src/store/common/SortUtil.spec.ts index 6581b640c..e3b5a4a6d 100644 --- a/webclient/src/store/common/SortUtil.spec.ts +++ b/webclient/src/store/common/SortUtil.spec.ts @@ -118,9 +118,9 @@ describe('sortByFields', () => { describe('sortUsersByField', () => { it('sorts by userLevel DESC first, then name ASC', () => { const users = [ - { name: 'Alice', userLevel: 1, accountageSecs: 0, privlevel: 0 }, - { name: 'Bob', userLevel: 8, accountageSecs: 0, privlevel: 0 }, - { name: 'Carol', userLevel: 1, accountageSecs: 0, privlevel: 0 }, + { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }, + { name: 'Bob', userLevel: 8, accountageSecs: 0n, privlevel: '' }, + { name: 'Carol', userLevel: 1, accountageSecs: 0n, privlevel: '' }, ]; SortUtil.sortUsersByField(users as any, { field: 'name', order: SortDirection.ASC }); expect(users[0].name).toBe('Bob'); @@ -136,8 +136,8 @@ describe('sortUsersByField', () => { it('returns 0 (stable) when two users tie on both userLevel and name', () => { const users = [ - { name: 'Alice', userLevel: 1, accountageSecs: 0, privlevel: 0 }, - { name: 'Alice', userLevel: 1, accountageSecs: 0, privlevel: 0 }, + { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }, + { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }, ]; expect(() => SortUtil.sortUsersByField(users as any, { field: 'name', order: SortDirection.ASC }) diff --git a/webclient/src/store/rooms/__mocks__/rooms-fixtures.ts b/webclient/src/store/rooms/__mocks__/rooms-fixtures.ts index 771fda745..27bc5a391 100644 --- a/webclient/src/store/rooms/__mocks__/rooms-fixtures.ts +++ b/webclient/src/store/rooms/__mocks__/rooms-fixtures.ts @@ -4,7 +4,6 @@ import { Room, SortDirection, User, - UserPrivLevel, UserSortField, } from 'types'; import { Message, RoomsState } from '../rooms.interfaces'; @@ -12,8 +11,8 @@ import { Message, RoomsState } from '../rooms.interfaces'; export function makeUser(overrides: Partial = {}): User { return { name: 'TestUser', - accountageSecs: 0, - privlevel: UserPrivLevel.NONE, + accountageSecs: 0n, + privlevel: '', userLevel: 0, ...overrides, }; diff --git a/webclient/src/store/server/__mocks__/server-fixtures.ts b/webclient/src/store/server/__mocks__/server-fixtures.ts index 9a7580365..fdaa997db 100644 --- a/webclient/src/store/server/__mocks__/server-fixtures.ts +++ b/webclient/src/store/server/__mocks__/server-fixtures.ts @@ -7,7 +7,6 @@ import { SortDirection, StatusEnum, User, - UserPrivLevel, UserSortField, WebSocketConnectOptions, WarnHistoryItem, @@ -18,8 +17,8 @@ import { ServerState } from '../server.interfaces'; export function makeUser(overrides: Partial = {}): User { return { name: 'TestUser', - accountageSecs: 0, - privlevel: UserPrivLevel.NONE, + accountageSecs: 0n, + privlevel: '', userLevel: 0, ...overrides, }; diff --git a/webclient/src/types/game.ts b/webclient/src/types/game.ts index 74ad18381..f15f62118 100644 --- a/webclient/src/types/game.ts +++ b/webclient/src/types/game.ts @@ -1,11 +1,77 @@ -export interface Game { - description: string; - gameId: number; +// ── Imports from generated proto files ─────────────────────────────────────── + +import type { GameEventContext } from 'generated/proto/game_event_context_pb'; +import type { CardToMove, Command_MoveCard } from 'generated/proto/command_move_card_pb'; +import type { Command_DrawCards } from 'generated/proto/command_draw_cards_pb'; +import type { Command_RollDie } from 'generated/proto/command_roll_die_pb'; +import type { Command_Shuffle } from 'generated/proto/command_shuffle_pb'; +import type { Command_FlipCard } from 'generated/proto/command_flip_card_pb'; +import type { Command_AttachCard } from 'generated/proto/command_attach_card_pb'; +import type { Command_CreateToken } from 'generated/proto/command_create_token_pb'; +import type { Command_SetCardAttr } from 'generated/proto/command_set_card_attr_pb'; +import type { Command_SetCardCounter } from 'generated/proto/command_set_card_counter_pb'; +import type { Command_IncCardCounter } from 'generated/proto/command_inc_card_counter_pb'; +import type { Command_RevealCards } from 'generated/proto/command_reveal_cards_pb'; +import type { Command_DumpZone } from 'generated/proto/command_dump_zone_pb'; +import type { Command_ChangeZoneProperties } from 'generated/proto/command_change_zone_properties_pb'; +import type { Command_CreateArrow } from 'generated/proto/command_create_arrow_pb'; +import type { Command_DeleteArrow } from 'generated/proto/command_delete_arrow_pb'; +import type { Command_CreateCounter } from 'generated/proto/command_create_counter_pb'; +import type { Command_SetCounter } from 'generated/proto/command_set_counter_pb'; +import type { Command_IncCounter } from 'generated/proto/command_inc_counter_pb'; +import type { Command_DelCounter } from 'generated/proto/command_del_counter_pb'; +import type { Command_KickFromGame } from 'generated/proto/command_kick_from_game_pb'; +import type { Command_ReadyStart } from 'generated/proto/command_ready_start_pb'; +import type { Command_Mulligan } from 'generated/proto/command_mulligan_pb'; +import type { Command_DeckSelect } from 'generated/proto/command_deck_select_pb'; +import type { MoveCard_ToZone } from 'generated/proto/move_card_to_zone_pb'; +import type { Command_SetSideboardPlan } from 'generated/proto/command_set_sideboard_plan_pb'; +import type { Command_SetSideboardLock } from 'generated/proto/command_set_sideboard_lock_pb'; +import type { Command_SetActivePhase } from 'generated/proto/command_set_active_phase_pb'; +import type { Command_GameSay } from 'generated/proto/command_game_say_pb'; +import type { Event_GameStateChanged } from 'generated/proto/event_game_state_changed_pb'; +import type { Event_GameSay } from 'generated/proto/event_game_say_pb'; +import type { Event_MoveCard } from 'generated/proto/event_move_card_pb'; +import type { Event_FlipCard } from 'generated/proto/event_flip_card_pb'; +import type { Event_DestroyCard } from 'generated/proto/event_destroy_card_pb'; +import type { Event_AttachCard } from 'generated/proto/event_attach_card_pb'; +import type { Event_CreateToken } from 'generated/proto/event_create_token_pb'; +import type { Event_SetCardAttr } from 'generated/proto/event_set_card_attr_pb'; +import type { Event_SetCardCounter } from 'generated/proto/event_set_card_counter_pb'; +import type { Event_CreateArrow } from 'generated/proto/event_create_arrow_pb'; +import type { Event_DeleteArrow } from 'generated/proto/event_delete_arrow_pb'; +import type { Event_CreateCounter } from 'generated/proto/event_create_counter_pb'; +import type { Event_SetCounter } from 'generated/proto/event_set_counter_pb'; +import type { Event_DelCounter } from 'generated/proto/event_del_counter_pb'; +import type { Event_DrawCards } from 'generated/proto/event_draw_cards_pb'; +import type { Event_RevealCards } from 'generated/proto/event_reveal_cards_pb'; +import type { Event_Shuffle } from 'generated/proto/event_shuffle_pb'; +import type { Event_RollDie } from 'generated/proto/event_roll_die_pb'; +import type { Event_DumpZone } from 'generated/proto/event_dump_zone_pb'; +import type { Event_ChangeZoneProperties } from 'generated/proto/event_change_zone_properties_pb'; +import type { Event_SetActivePlayer } from 'generated/proto/event_set_active_player_pb'; +import type { Event_SetActivePhase } from 'generated/proto/event_set_active_phase_pb'; +import type { Event_ReverseTurn } from 'generated/proto/event_reverse_turn_pb'; +import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb'; +import type { color } from 'generated/proto/color_pb'; +import type { ServerInfo_CardCounter } from 'generated/proto/serverinfo_cardcounter_pb'; +import type { ServerInfo_Card } from 'generated/proto/serverinfo_card_pb'; +import type { ServerInfo_Zone } from 'generated/proto/serverinfo_zone_pb'; +import type { ServerInfo_Counter } from 'generated/proto/serverinfo_counter_pb'; +import type { ServerInfo_Arrow } from 'generated/proto/serverinfo_arrow_pb'; +import type { ServerInfo_PlayerProperties } from 'generated/proto/serverinfo_playerproperties_pb'; +import type { ServerInfo_Player } from 'generated/proto/serverinfo_player_pb'; + +// ── Enum re-exports from generated proto files ──────────────────────────────── + +export { CardAttribute } from 'generated/proto/card_attributes_pb'; +export { ServerInfo_Zone_ZoneType as ZoneType } from 'generated/proto/serverinfo_zone_pb'; + +// ── UI types (not proto mirrors) ────────────────────────────────────────────── + +export type Game = ServerInfo_Game & { gameType: string; - gameTypes: string[]; - roomId: number; - started: boolean; -} +}; export enum GameSortField { START_TIME = 'startTime' @@ -14,7 +80,7 @@ export enum GameSortField { export interface GameConfig { description: string; password: string; - maxPlayer: number; + maxPlayers: number; onlyBuddies: boolean; onlyRegistered: boolean; spectatorsAllowed: boolean; @@ -24,6 +90,8 @@ export interface GameConfig { gameTypeIds: number[]; joinAsJudge: boolean; joinAsSpectator: boolean; + startingLifeTotal?: number; + shareDecklistsOnLoad?: boolean; } export interface JoinGameParams { @@ -41,283 +109,52 @@ export enum LeaveGameReason { USER_DISCONNECTED = 4 } -// ── Enums ──────────────────────────────────────────────────────────────────── +// ── Type aliases for generated state mirror types ───────────────────────────── -export enum ZoneType { - PrivateZone = 0, - PublicZone = 1, - HiddenZone = 2, -} +export type Color = color; +export type CardCounterInfo = ServerInfo_CardCounter; +export type CardInfo = ServerInfo_Card; +export type ZoneInfo = ServerInfo_Zone; +export type CounterInfo = ServerInfo_Counter; +export type ArrowInfo = ServerInfo_Arrow; +export type PlayerProperties = ServerInfo_PlayerProperties; +export type PlayerInfo = ServerInfo_Player; -/** Matches CardAttribute enum in card_attributes.proto */ -export enum CardAttribute { - AttrTapped = 1, - AttrAttacking = 2, - AttrFaceDown = 3, - AttrColor = 4, - AttrPT = 5, - AttrAnnotation = 6, - AttrDoesntUntap = 7, -} +// ── Type aliases for generated event data types ─────────────────────────────── -// ── Primitive data structures (mirrors ServerInfo_* protos) ────────────────── +export type GameStateChangedData = Event_GameStateChanged; +export type GameSayData = Event_GameSay; +export type MoveCardData = Event_MoveCard; +export type FlipCardData = Event_FlipCard; +export type DestroyCardData = Event_DestroyCard; +export type AttachCardData = Event_AttachCard; +export type CreateTokenData = Event_CreateToken; +export type SetCardAttrData = Event_SetCardAttr; +export type SetCardCounterData = Event_SetCardCounter; +export type CreateArrowData = Event_CreateArrow; +export type DeleteArrowData = Event_DeleteArrow; +export type CreateCounterData = Event_CreateCounter; +export type SetCounterData = Event_SetCounter; +export type DelCounterData = Event_DelCounter; +export type DrawCardsData = Event_DrawCards; +export type RevealCardsData = Event_RevealCards; +export type ShuffleData = Event_Shuffle; +export type RollDieData = Event_RollDie; +export type DumpZoneData = Event_DumpZone; +export type ChangeZonePropertiesData = Event_ChangeZoneProperties; +export type SetActivePlayerData = Event_SetActivePlayer; +export type SetActivePhaseData = Event_SetActivePhase; +export type ReverseTurnData = Event_ReverseTurn; -export interface Color { - r: number; - g: number; - b: number; - a: number; -} +// ── GameEventContext (re-export of generated type) ──────────────────────────── -/** Mirrors ServerInfo_CardCounter */ -export interface CardCounterInfo { - id: number; - value: number; -} - -/** Mirrors ServerInfo_Card */ -export interface CardInfo { - id: number; - name: string; - x: number; - y: number; - faceDown: boolean; - tapped: boolean; - attacking: boolean; - color: string; - pt: string; - annotation: string; - destroyOnZoneChange: boolean; - doesntUntap: boolean; - counterList: CardCounterInfo[]; - attachPlayerId: number; - attachZone: string; - attachCardId: number; - providerId: string; -} - -/** Mirrors ServerInfo_Zone */ -export interface ZoneInfo { - name: string; - type: ZoneType; - withCoords: boolean; - cardCount: number; - cardList: CardInfo[]; - alwaysRevealTopCard: boolean; - alwaysLookAtTopCard: boolean; -} - -/** Mirrors ServerInfo_Counter */ -export interface CounterInfo { - id: number; - name: string; - counterColor: Color; - radius: number; - count: number; -} - -/** Mirrors ServerInfo_Arrow */ -export interface ArrowInfo { - id: number; - startPlayerId: number; - startZone: string; - startCardId: number; - targetPlayerId: number; - targetZone: string; - targetCardId: number; - arrowColor: Color; -} - -/** Mirrors ServerInfo_PlayerProperties */ -export interface PlayerProperties { - playerId: number; - userInfo: any; - spectator: boolean; - conceded: boolean; - readyStart: boolean; - deckHash: string; - pingSeconds: number; - sideboardLocked: boolean; - judge: boolean; -} - -/** Mirrors ServerInfo_Player */ -export interface PlayerInfo { - properties: PlayerProperties; - deckList: string; - zoneList: ZoneInfo[]; - counterList: CounterInfo[]; - arrowList: ArrowInfo[]; -} - -// ── Game event payload interfaces (data arriving from server events) ────────── - -export interface GameStateChangedData { - playerList: PlayerInfo[]; - gameStarted: boolean; - activePlayerId: number; - activePhase: number; - secondsElapsed: number; -} - -export interface GameSayData { - message: string; -} - -export interface MoveCardData { - cardId: number; - cardName: string; - startPlayerId: number; - startZone: string; - position: number; - targetPlayerId: number; - targetZone: string; - x: number; - y: number; - newCardId: number; - faceDown: boolean; - newCardProviderId: string; -} - -export interface FlipCardData { - zoneName: string; - cardId: number; - cardName: string; - faceDown: boolean; - cardProviderId: string; -} - -export interface DestroyCardData { - zoneName: string; - cardId: number; -} - -export interface AttachCardData { - startZone: string; - cardId: number; - targetPlayerId: number; - targetZone: string; - targetCardId: number; -} - -export interface CreateTokenData { - zoneName: string; - cardId: number; - cardName: string; - color: string; - pt: string; - annotation: string; - destroyOnZoneChange: boolean; - x: number; - y: number; - cardProviderId: string; - faceDown: boolean; -} - -export interface SetCardAttrData { - zoneName: string; - cardId: number; - attribute: CardAttribute; - attrValue: string; -} - -export interface SetCardCounterData { - zoneName: string; - cardId: number; - counterId: number; - counterValue: number; -} - -export interface CreateArrowData { - arrowInfo: ArrowInfo; -} - -export interface DeleteArrowData { - arrowId: number; -} - -export interface CreateCounterData { - counterInfo: CounterInfo; -} - -export interface SetCounterData { - counterId: number; - value: number; -} - -export interface DelCounterData { - counterId: number; -} - -export interface DrawCardsData { - number: number; - cards: CardInfo[]; -} - -export interface RevealCardsData { - zoneName: string; - cardId: number[]; - otherPlayerId: number; - cards: CardInfo[]; - grantWriteAccess: boolean; - numberOfCards: number; -} - -export interface ShuffleData { - zoneName: string; - start: number; - end: number; -} - -export interface RollDieData { - sides: number; - value: number; - values: number[]; -} - -export interface DumpZoneData { - zoneOwnerId: number; - zoneName: string; - numberCards: number; - isReversed: boolean; -} - -export interface ChangeZonePropertiesData { - zoneName: string; - alwaysRevealTopCard: boolean; - alwaysLookAtTopCard: boolean; -} - -export interface SetActivePlayerData { - activePlayerId: number; -} - -export interface SetActivePhaseData { - phase: number; -} - -export interface ReverseTurnData { - reversed: boolean; -} +export type { GameEventContext }; /** * Passed to every game event handler alongside the event payload. * Contains per-container metadata from GameEventContainer. * Not stored in Redux — transient routing metadata only. */ -export interface GameEventContext { - '.Context_ReadyStart.ext'?: {}; - '.Context_Concede.ext'?: {}; - '.Context_DeckSelect.ext'?: {}; - '.Context_UndoDraw.ext'?: {}; - '.Context_MoveCard.ext'?: {}; - '.Context_Mulligan.ext'?: {}; - '.Context_PingChanged.ext'?: {}; - '.Context_ConnectionStateChanged.ext'?: {}; - '.Context_SetSideboardLock.ext'?: {}; - '.Context_Unconcede.ext'?: {}; -} - export interface GameEventMeta { gameId: number; playerId: number; @@ -328,186 +165,34 @@ export interface GameEventMeta { forcedByJudge: number; } -// ── Command parameter interfaces ───────────────────────────────────────────── +// ── Type aliases for generated command param types ──────────────────────────── -export interface CardToMove { - cardId: number; - faceDown?: boolean; - pt?: string; - tapped?: boolean; -} - -export interface MoveCardParams { - startPlayerId: number; - startZone: string; - cardsToMove: { card: CardToMove[] }; - targetPlayerId: number; - targetZone: string; - x?: number; - y?: number; - isReversed?: boolean; -} - -export interface DrawCardsParams { - number: number; -} - -export interface RollDieParams { - sides: number; - count?: number; -} - -export interface ShuffleParams { - zoneName: string; - start?: number; - end?: number; -} - -export interface FlipCardParams { - zone: string; - cardId: number; - faceDown: boolean; - pt?: string; -} - -export interface AttachCardParams { - startZone: string; - cardId: number; - targetPlayerId?: number; - targetZone?: string; - targetCardId?: number; -} - -export interface CreateTokenParams { - zone: string; - cardName: string; - color?: string; - pt?: string; - annotation?: string; - destroyOnZoneChange?: boolean; - x?: number; - y?: number; - targetZone?: string; - targetCardId?: number; - targetMode?: number; - cardProviderId?: string; - faceDown?: boolean; -} - -export interface SetCardAttrParams { - zone: string; - cardId: number; - attribute: CardAttribute; - attrValue: string; -} - -export interface SetCardCounterParams { - zone: string; - cardId: number; - counterId: number; - counterValue: number; -} - -export interface IncCardCounterParams { - zone: string; - cardId: number; - counterId: number; - counterDelta: number; -} - -export interface RevealCardsParams { - zoneName: string; - cardId?: number[]; - playerId?: number; - grantWriteAccess?: boolean; - topCards?: number; -} - -export interface DumpZoneParams { - playerId: number; - zoneName: string; - numberCards: number; - isReversed?: boolean; -} - -export interface ChangeZonePropertiesParams { - zoneName: string; - alwaysRevealTopCard?: boolean; - alwaysLookAtTopCard?: boolean; -} - -export interface CreateArrowParams { - startPlayerId: number; - startZone: string; - startCardId: number; - targetPlayerId: number; - targetZone?: string; - targetCardId?: number; - arrowColor: Color; - deleteInPhase?: number; -} - -export interface DeleteArrowParams { - arrowId: number; -} - -export interface CreateCounterParams { - counterName: string; - counterColor: Color; - radius: number; - value: number; -} - -export interface SetCounterParams { - counterId: number; - value: number; -} - -export interface IncCounterParams { - counterId: number; - delta: number; -} - -export interface DelCounterParams { - counterId: number; -} - -export interface KickFromGameParams { - playerId: number; -} - -export interface ReadyStartParams { - ready: boolean; - forceStart?: boolean; -} - -export interface MulliganParams { - number: number; -} - -export interface DeckSelectParams { - deck?: string; - deckId?: number; -} - -export interface MoveCardToZone { - cardName: string; - startZone: string; - targetZone: string; -} - -export interface SetSideboardPlanParams { - moveList: MoveCardToZone[]; -} - -export interface SetSideboardLockParams { - locked: boolean; -} - -export interface SetActivePhaseParams { - phase: number; -} - -export interface GameSayParams { - message: string; -} +export type { CardToMove }; +export type MoveCardParams = Command_MoveCard; +export type DrawCardsParams = Command_DrawCards; +export type RollDieParams = Command_RollDie; +export type ShuffleParams = Command_Shuffle; +export type FlipCardParams = Command_FlipCard; +export type AttachCardParams = Command_AttachCard; +export type CreateTokenParams = Command_CreateToken; +export type SetCardAttrParams = Command_SetCardAttr; +export type SetCardCounterParams = Command_SetCardCounter; +export type IncCardCounterParams = Command_IncCardCounter; +export type RevealCardsParams = Command_RevealCards; +export type DumpZoneParams = Command_DumpZone; +export type ChangeZonePropertiesParams = Command_ChangeZoneProperties; +export type CreateArrowParams = Command_CreateArrow; +export type DeleteArrowParams = Command_DeleteArrow; +export type CreateCounterParams = Command_CreateCounter; +export type SetCounterParams = Command_SetCounter; +export type IncCounterParams = Command_IncCounter; +export type DelCounterParams = Command_DelCounter; +export type KickFromGameParams = Command_KickFromGame; +export type ReadyStartParams = Command_ReadyStart; +export type MulliganParams = Command_Mulligan; +export type DeckSelectParams = Command_DeckSelect; +export type MoveCardToZone = MoveCard_ToZone; +export type SetSideboardPlanParams = Command_SetSideboardPlan; +export type SetSideboardLockParams = Command_SetSideboardLock; +export type SetActivePhaseParams = Command_SetActivePhase; +export type GameSayParams = Command_GameSay; diff --git a/webclient/src/types/message.ts b/webclient/src/types/message.ts index d7f256011..d4dc282a1 100644 --- a/webclient/src/types/message.ts +++ b/webclient/src/types/message.ts @@ -1,7 +1,5 @@ -export interface Message { - name: string; - message: string; - messageType: number; - timeOf: number; +import type { Event_RoomSay } from 'generated/proto/event_room_say_pb'; + +export type Message = Event_RoomSay & { timeReceived: number; -} +}; diff --git a/webclient/src/types/replay.ts b/webclient/src/types/replay.ts index dfa78538a..5f2e19217 100644 --- a/webclient/src/types/replay.ts +++ b/webclient/src/types/replay.ts @@ -1,16 +1,5 @@ -export interface Replay { - replayId: number; - replayName: string; - duration: number; -} +import type { ServerInfo_Replay } from 'generated/proto/serverinfo_replay_pb'; +import type { ServerInfo_ReplayMatch } from 'generated/proto/serverinfo_replay_match_pb'; -export interface ReplayMatch { - replayList: Replay[]; - gameId: number; - roomName: string; - timeStarted: number; - length: number; - gameName: string; - playerNames: string[]; - doNotHide: boolean; -} +export type Replay = ServerInfo_Replay; +export type ReplayMatch = ServerInfo_ReplayMatch; diff --git a/webclient/src/types/room.ts b/webclient/src/types/room.ts index e69b1a499..9f18d2d4c 100644 --- a/webclient/src/types/room.ts +++ b/webclient/src/types/room.ts @@ -1,23 +1,8 @@ -import { User } from './user'; - -export interface Room { - autoJoin: boolean - description: string; - gameCount: number; - gameList: any[]; - gametypeList: any[]; - gametypeMap: GametypeMap; - name: string; - permissionlevel: RoomAccessLevel; - playerCount: number; - privilegelevel: RoomAccessLevel; - roomId: number; - userList: User[]; - order: number; -} +import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb'; export interface GametypeMap { [index: number]: string } -export enum RoomAccessLevel { - 'none' -} +export type Room = ServerInfo_Room & { + gametypeMap: GametypeMap; + order: number; +}; diff --git a/webclient/src/types/session.ts b/webclient/src/types/session.ts index 1b49616c5..a7a615c4a 100644 --- a/webclient/src/types/session.ts +++ b/webclient/src/types/session.ts @@ -1,7 +1 @@ -export enum NotificationType { - UNKNOWN = 0, - PROMOTED = 1, - WARNING = 2, - IDLEWARNING = 3, - CUSTOM = 4, -}; +export { Event_NotifyUser_NotificationType as NotificationType } from 'generated/proto/event_notify_user_pb'; diff --git a/webclient/src/types/user.ts b/webclient/src/types/user.ts index 2d0383eb7..f3db25849 100644 --- a/webclient/src/types/user.ts +++ b/webclient/src/types/user.ts @@ -1,27 +1,7 @@ -export interface User { - accountageSecs: number; - name: string; - privlevel: UserPrivLevel; - userLevel: number; - realName?: string; - country?: string; - avatarBmp?: Uint8Array; -} +import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; -export enum UserLevelFlag { - IsNothing = 0, - IsUser = 1, - IsRegistered = 2, - IsModerator = 4, - IsAdmin = 8, - IsJudge = 16, -} - -export enum UserPrivLevel { - NONE = 0, - VIP = 1, - DONOR = 2 -} +export type User = ServerInfo_User; +export { ServerInfo_User_UserLevelFlag as UserLevelFlag } from 'generated/proto/serverinfo_user_pb'; export enum UserSortField { NAME = 'name' diff --git a/webclient/src/websocket/WebClient.spec.ts b/webclient/src/websocket/WebClient.spec.ts index 20cac3bcb..b279132ea 100644 --- a/webclient/src/websocket/WebClient.spec.ts +++ b/webclient/src/websocket/WebClient.spec.ts @@ -17,7 +17,7 @@ vi.mock('./services/ProtobufService', () => ({ vi.mock('./persistence', () => ({ RoomPersistence: { clearStore: vi.fn() }, - SessionPersistence: { clearStore: vi.fn() }, + SessionPersistence: { clearStore: vi.fn(), initialized: vi.fn() }, })); import { WebClient } from './WebClient'; @@ -60,6 +60,10 @@ describe('WebClient', () => { messageSubject.next(event); expect(client.protobuf.handleMessageEvent).toHaveBeenCalledWith(event); }); + + it('calls SessionPersistence.initialized', () => { + expect(SessionPersistence.initialized).toHaveBeenCalled(); + }); }); describe('connect', () => { diff --git a/webclient/src/websocket/WebClient.ts b/webclient/src/websocket/WebClient.ts index 378b5fd62..e1a0fad56 100644 --- a/webclient/src/websocket/WebClient.ts +++ b/webclient/src/websocket/WebClient.ts @@ -47,6 +47,8 @@ export class WebClient { this.protobuf.handleMessageEvent(message); }); + SessionPersistence.initialized(); + if (import.meta.env.MODE !== 'test') { console.log(this); } diff --git a/webclient/src/websocket/__mocks__/callbackHelpers.ts b/webclient/src/websocket/__mocks__/callbackHelpers.ts index 7f7d37ecc..0aaa61184 100644 --- a/webclient/src/websocket/__mocks__/callbackHelpers.ts +++ b/webclient/src/websocket/__mocks__/callbackHelpers.ts @@ -4,8 +4,8 @@ * @param mockFn - The vi.Mock for the BackendService send method * (e.g. BackendService.sendSessionCommand as vi.Mock). * @param optsArgIndex - Index of the options argument in the mock call. - * Defaults to 2 (commandName, params, options). - * Use 3 for sendRoomCommand (roomId, commandName, params, options). + * Defaults to 2 (ext, value, options). + * Use 3 for sendRoomCommand (roomId, ext, value, options). */ export function makeCallbackHelpers(mockFn: vi.Mock, optsArgIndex = 2) { function getLastSendOpts() { diff --git a/webclient/src/websocket/__mocks__/helpers.ts b/webclient/src/websocket/__mocks__/helpers.ts index dc1da0a73..c9d6d9973 100644 --- a/webclient/src/websocket/__mocks__/helpers.ts +++ b/webclient/src/websocket/__mocks__/helpers.ts @@ -1,54 +1,9 @@ /** * Shared mock factories for websocket layer unit tests. * Import the helpers you need in each spec file via: - * import { makeMockProtoRoot, makeMockWebSocket } from '../__mocks__/helpers'; + * import { makeMockWebSocket } from '../__mocks__/helpers'; */ -/** Builds a minimal mock of ProtoController.root */ -export function makeMockProtoRoot() { - const encode = { finish: vi.fn().mockReturnValue(new Uint8Array()) }; - return { - CommandContainer: { - create: vi.fn(args => ({ ...args })), - encode: vi.fn().mockReturnValue(encode), - }, - SessionCommand: { create: vi.fn(args => ({ ...args })) }, - RoomCommand: { create: vi.fn(args => ({ ...args })) }, - ModeratorCommand: { create: vi.fn(args => ({ ...args })) }, - AdminCommand: { create: vi.fn(args => ({ ...args })) }, - ServerMessage: { - decode: vi.fn(), - MessageType: { - RESPONSE: 'RESPONSE', - ROOM_EVENT: 'ROOM_EVENT', - SESSION_EVENT: 'SESSION_EVENT', - GAME_EVENT_CONTAINER: 'GAME_EVENT_CONTAINER', - }, - }, - Response: { - ResponseCode: { - RespOk: 0, - RespRegistrationRequired: 1, - }, - }, - Event_ServerIdentification: { - ServerOptions: { SupportsPasswordHash: 2 }, - }, - Event_ConnectionClosed: { - CloseReason: { - USER_LIMIT_REACHED: 1, - TOO_MANY_CONNECTIONS: 2, - BANNED: 3, - DEMOTED: 4, - SERVER_SHUTDOWN: 5, - USERNAMEINVALID: 6, - LOGGEDINELSEWERE: 7, - OTHER: 8, - }, - }, - }; -} - /** Builds a mock WebSocket instance */ export function makeMockWebSocketInstance() { return { diff --git a/webclient/src/websocket/__mocks__/sessionCommandMocks.ts b/webclient/src/websocket/__mocks__/sessionCommandMocks.ts index d5bea3a7f..cbab4833c 100644 --- a/webclient/src/websocket/__mocks__/sessionCommandMocks.ts +++ b/webclient/src/websocket/__mocks__/sessionCommandMocks.ts @@ -25,38 +25,6 @@ export function makeWebClientMock() { }; } -/** Superset ProtoController.root mock — includes all ResponseCode values and Event_ServerIdentification. */ -export function makeProtoControllerRootMock() { - return { - Response: { - ResponseCode: { - RespOk: 0, - RespClientUpdateRequired: 1, - RespWrongPassword: 2, - RespUsernameInvalid: 3, - RespWouldOverwriteOldSession: 4, - RespUserIsBanned: 5, - RespRegistrationRequired: 6, - RespClientIdRequired: 7, - RespContextError: 8, - RespAccountNotActivated: 9, - RespRegistrationAccepted: 10, - RespRegistrationAcceptedNeedsActivation: 11, - RespUserAlreadyExists: 12, - RespPasswordTooShort: 13, - RespEmailRequiredToRegister: 14, - RespEmailBlackListed: 15, - RespTooManyRequests: 16, - RespRegistrationDisabled: 17, - RespActivationAccepted: 18, - }, - }, - Event_ServerIdentification: { - ServerOptions: { SupportsPasswordHash: 2 }, - }, - }; -} - /** Utils mock with unified return values. */ export function makeUtilsMock() { return { diff --git a/webclient/src/websocket/commands/admin/adjustMod.ts b/webclient/src/websocket/commands/admin/adjustMod.ts index fd71fece1..7b61e322a 100644 --- a/webclient/src/websocket/commands/admin/adjustMod.ts +++ b/webclient/src/websocket/commands/admin/adjustMod.ts @@ -1,8 +1,10 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_AdjustMod_ext, Command_AdjustModSchema } from 'generated/proto/admin_commands_pb'; import { AdminPersistence } from '../../persistence'; export function adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void { - BackendService.sendAdminCommand('Command_AdjustMod', { userName, shouldBeMod, shouldBeJudge }, { + BackendService.sendAdminCommand(Command_AdjustMod_ext, create(Command_AdjustModSchema, { userName, shouldBeMod, shouldBeJudge }), { onSuccess: () => { AdminPersistence.adjustMod(userName, shouldBeMod, shouldBeJudge); }, diff --git a/webclient/src/websocket/commands/admin/adminCommands.spec.ts b/webclient/src/websocket/commands/admin/adminCommands.spec.ts index be0124736..098db2597 100644 --- a/webclient/src/websocket/commands/admin/adminCommands.spec.ts +++ b/webclient/src/websocket/commands/admin/adminCommands.spec.ts @@ -22,7 +22,8 @@ import { shutdownServer } from './shutdownServer'; import { updateServerMessage } from './updateServerMessage'; const { getLastSendOpts, invokeOnSuccess } = makeCallbackHelpers( - BackendService.sendAdminCommand as vi.Mock + BackendService.sendAdminCommand as vi.Mock, + 2 ); beforeEach(() => vi.clearAllMocks()); @@ -34,11 +35,7 @@ describe('adjustMod', () => { it('calls sendAdminCommand with Command_AdjustMod', () => { adjustMod('alice', true, false); - expect(BackendService.sendAdminCommand).toHaveBeenCalledWith( - 'Command_AdjustMod', - expect.objectContaining({ userName: 'alice', shouldBeMod: true, shouldBeJudge: false }), - expect.any(Object) - ); + expect(BackendService.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object)); }); it('onSuccess calls AdminPersistence.adjustMod', () => { @@ -55,7 +52,7 @@ describe('reloadConfig', () => { it('calls sendAdminCommand with Command_ReloadConfig', () => { reloadConfig(); - expect(BackendService.sendAdminCommand).toHaveBeenCalledWith('Command_ReloadConfig', {}, expect.any(Object)); + expect(BackendService.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object)); }); it('onSuccess calls AdminPersistence.reloadConfig', () => { @@ -72,11 +69,7 @@ describe('shutdownServer', () => { it('calls sendAdminCommand with Command_ShutdownServer', () => { shutdownServer('maintenance', 10); - expect(BackendService.sendAdminCommand).toHaveBeenCalledWith( - 'Command_ShutdownServer', - { reason: 'maintenance', minutes: 10 }, - expect.any(Object) - ); + expect(BackendService.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object)); }); it('onSuccess calls AdminPersistence.shutdownServer', () => { @@ -93,7 +86,7 @@ describe('updateServerMessage', () => { it('calls sendAdminCommand with Command_UpdateServerMessage', () => { updateServerMessage(); - expect(BackendService.sendAdminCommand).toHaveBeenCalledWith('Command_UpdateServerMessage', {}, expect.any(Object)); + expect(BackendService.sendAdminCommand).toHaveBeenCalledWith(expect.any(Object), expect.any(Object), expect.any(Object)); }); it('onSuccess calls AdminPersistence.updateServerMessage', () => { diff --git a/webclient/src/websocket/commands/admin/reloadConfig.ts b/webclient/src/websocket/commands/admin/reloadConfig.ts index 979f3ec73..e72735c60 100644 --- a/webclient/src/websocket/commands/admin/reloadConfig.ts +++ b/webclient/src/websocket/commands/admin/reloadConfig.ts @@ -1,8 +1,10 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ReloadConfig_ext, Command_ReloadConfigSchema } from 'generated/proto/admin_commands_pb'; import { AdminPersistence } from '../../persistence'; export function reloadConfig(): void { - BackendService.sendAdminCommand('Command_ReloadConfig', {}, { + BackendService.sendAdminCommand(Command_ReloadConfig_ext, create(Command_ReloadConfigSchema), { onSuccess: () => { AdminPersistence.reloadConfig(); }, diff --git a/webclient/src/websocket/commands/admin/shutdownServer.ts b/webclient/src/websocket/commands/admin/shutdownServer.ts index e65c900db..7fc32fb24 100644 --- a/webclient/src/websocket/commands/admin/shutdownServer.ts +++ b/webclient/src/websocket/commands/admin/shutdownServer.ts @@ -1,8 +1,10 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ShutdownServer_ext, Command_ShutdownServerSchema } from 'generated/proto/admin_commands_pb'; import { AdminPersistence } from '../../persistence'; export function shutdownServer(reason: string, minutes: number): void { - BackendService.sendAdminCommand('Command_ShutdownServer', { reason, minutes }, { + BackendService.sendAdminCommand(Command_ShutdownServer_ext, create(Command_ShutdownServerSchema, { reason, minutes }), { onSuccess: () => { AdminPersistence.shutdownServer(); }, diff --git a/webclient/src/websocket/commands/admin/updateServerMessage.ts b/webclient/src/websocket/commands/admin/updateServerMessage.ts index e2b194514..d81b63028 100644 --- a/webclient/src/websocket/commands/admin/updateServerMessage.ts +++ b/webclient/src/websocket/commands/admin/updateServerMessage.ts @@ -1,8 +1,10 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_UpdateServerMessage_ext, Command_UpdateServerMessageSchema } from 'generated/proto/admin_commands_pb'; import { AdminPersistence } from '../../persistence'; export function updateServerMessage(): void { - BackendService.sendAdminCommand('Command_UpdateServerMessage', {}, { + BackendService.sendAdminCommand(Command_UpdateServerMessage_ext, create(Command_UpdateServerMessageSchema), { onSuccess: () => { AdminPersistence.updateServerMessage(); }, diff --git a/webclient/src/websocket/commands/game/attachCard.ts b/webclient/src/websocket/commands/game/attachCard.ts index bc9ebce75..08162f651 100644 --- a/webclient/src/websocket/commands/game/attachCard.ts +++ b/webclient/src/websocket/commands/game/attachCard.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_AttachCardSchema, Command_AttachCard_ext } from 'generated/proto/command_attach_card_pb'; import { AttachCardParams } from 'types'; export function attachCard(gameId: number, params: AttachCardParams): void { - BackendService.sendGameCommand(gameId, 'Command_AttachCard', params); + BackendService.sendGameCommand(gameId, Command_AttachCard_ext, create(Command_AttachCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/changeZoneProperties.ts b/webclient/src/websocket/commands/game/changeZoneProperties.ts index 77167cc72..7ee1feaca 100644 --- a/webclient/src/websocket/commands/game/changeZoneProperties.ts +++ b/webclient/src/websocket/commands/game/changeZoneProperties.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ChangeZonePropertiesSchema, Command_ChangeZoneProperties_ext } from 'generated/proto/command_change_zone_properties_pb'; import { ChangeZonePropertiesParams } from 'types'; export function changeZoneProperties(gameId: number, params: ChangeZonePropertiesParams): void { - BackendService.sendGameCommand(gameId, 'Command_ChangeZoneProperties', params); + BackendService.sendGameCommand(gameId, Command_ChangeZoneProperties_ext, create(Command_ChangeZonePropertiesSchema, params)); } diff --git a/webclient/src/websocket/commands/game/concede.ts b/webclient/src/websocket/commands/game/concede.ts index fd587464b..50e9c6d29 100644 --- a/webclient/src/websocket/commands/game/concede.ts +++ b/webclient/src/websocket/commands/game/concede.ts @@ -1,5 +1,7 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ConcedeSchema, Command_Concede_ext } from 'generated/proto/command_concede_pb'; export function concede(gameId: number): void { - BackendService.sendGameCommand(gameId, 'Command_Concede', {}); + BackendService.sendGameCommand(gameId, Command_Concede_ext, create(Command_ConcedeSchema)); } diff --git a/webclient/src/websocket/commands/game/createArrow.ts b/webclient/src/websocket/commands/game/createArrow.ts index 7b3a8a294..0a641585b 100644 --- a/webclient/src/websocket/commands/game/createArrow.ts +++ b/webclient/src/websocket/commands/game/createArrow.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_CreateArrowSchema, Command_CreateArrow_ext } from 'generated/proto/command_create_arrow_pb'; import { CreateArrowParams } from 'types'; export function createArrow(gameId: number, params: CreateArrowParams): void { - BackendService.sendGameCommand(gameId, 'Command_CreateArrow', params); + BackendService.sendGameCommand(gameId, Command_CreateArrow_ext, create(Command_CreateArrowSchema, params)); } diff --git a/webclient/src/websocket/commands/game/createCounter.ts b/webclient/src/websocket/commands/game/createCounter.ts index 562ac06b9..1bbf99478 100644 --- a/webclient/src/websocket/commands/game/createCounter.ts +++ b/webclient/src/websocket/commands/game/createCounter.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_CreateCounterSchema, Command_CreateCounter_ext } from 'generated/proto/command_create_counter_pb'; import { CreateCounterParams } from 'types'; export function createCounter(gameId: number, params: CreateCounterParams): void { - BackendService.sendGameCommand(gameId, 'Command_CreateCounter', params); + BackendService.sendGameCommand(gameId, Command_CreateCounter_ext, create(Command_CreateCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/createToken.ts b/webclient/src/websocket/commands/game/createToken.ts index 16e170e70..4b554e688 100644 --- a/webclient/src/websocket/commands/game/createToken.ts +++ b/webclient/src/websocket/commands/game/createToken.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_CreateTokenSchema, Command_CreateToken_ext } from 'generated/proto/command_create_token_pb'; import { CreateTokenParams } from 'types'; export function createToken(gameId: number, params: CreateTokenParams): void { - BackendService.sendGameCommand(gameId, 'Command_CreateToken', params); + BackendService.sendGameCommand(gameId, Command_CreateToken_ext, create(Command_CreateTokenSchema, params)); } diff --git a/webclient/src/websocket/commands/game/deckSelect.ts b/webclient/src/websocket/commands/game/deckSelect.ts index 077cee580..60a874150 100644 --- a/webclient/src/websocket/commands/game/deckSelect.ts +++ b/webclient/src/websocket/commands/game/deckSelect.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_DeckSelectSchema, Command_DeckSelect_ext } from 'generated/proto/command_deck_select_pb'; import { DeckSelectParams } from 'types'; export function deckSelect(gameId: number, params: DeckSelectParams): void { - BackendService.sendGameCommand(gameId, 'Command_DeckSelect', params); + BackendService.sendGameCommand(gameId, Command_DeckSelect_ext, create(Command_DeckSelectSchema, params)); } diff --git a/webclient/src/websocket/commands/game/delCounter.ts b/webclient/src/websocket/commands/game/delCounter.ts index ade08ef21..b84fb5a0f 100644 --- a/webclient/src/websocket/commands/game/delCounter.ts +++ b/webclient/src/websocket/commands/game/delCounter.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_DelCounterSchema, Command_DelCounter_ext } from 'generated/proto/command_del_counter_pb'; import { DelCounterParams } from 'types'; export function delCounter(gameId: number, params: DelCounterParams): void { - BackendService.sendGameCommand(gameId, 'Command_DelCounter', params); + BackendService.sendGameCommand(gameId, Command_DelCounter_ext, create(Command_DelCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/deleteArrow.ts b/webclient/src/websocket/commands/game/deleteArrow.ts index fceef8a95..67929e2a2 100644 --- a/webclient/src/websocket/commands/game/deleteArrow.ts +++ b/webclient/src/websocket/commands/game/deleteArrow.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_DeleteArrowSchema, Command_DeleteArrow_ext } from 'generated/proto/command_delete_arrow_pb'; import { DeleteArrowParams } from 'types'; export function deleteArrow(gameId: number, params: DeleteArrowParams): void { - BackendService.sendGameCommand(gameId, 'Command_DeleteArrow', params); + BackendService.sendGameCommand(gameId, Command_DeleteArrow_ext, create(Command_DeleteArrowSchema, params)); } diff --git a/webclient/src/websocket/commands/game/drawCards.ts b/webclient/src/websocket/commands/game/drawCards.ts index ae8e80744..ad5a08771 100644 --- a/webclient/src/websocket/commands/game/drawCards.ts +++ b/webclient/src/websocket/commands/game/drawCards.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_DrawCardsSchema, Command_DrawCards_ext } from 'generated/proto/command_draw_cards_pb'; import { DrawCardsParams } from 'types'; export function drawCards(gameId: number, params: DrawCardsParams): void { - BackendService.sendGameCommand(gameId, 'Command_DrawCards', params); + BackendService.sendGameCommand(gameId, Command_DrawCards_ext, create(Command_DrawCardsSchema, params)); } diff --git a/webclient/src/websocket/commands/game/dumpZone.ts b/webclient/src/websocket/commands/game/dumpZone.ts index 18aec44f6..9075b2dcf 100644 --- a/webclient/src/websocket/commands/game/dumpZone.ts +++ b/webclient/src/websocket/commands/game/dumpZone.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_DumpZoneSchema, Command_DumpZone_ext } from 'generated/proto/command_dump_zone_pb'; import { DumpZoneParams } from 'types'; export function dumpZone(gameId: number, params: DumpZoneParams): void { - BackendService.sendGameCommand(gameId, 'Command_DumpZone', params); + BackendService.sendGameCommand(gameId, Command_DumpZone_ext, create(Command_DumpZoneSchema, params)); } diff --git a/webclient/src/websocket/commands/game/flipCard.ts b/webclient/src/websocket/commands/game/flipCard.ts index 907f1e6a4..c12729d46 100644 --- a/webclient/src/websocket/commands/game/flipCard.ts +++ b/webclient/src/websocket/commands/game/flipCard.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_FlipCardSchema, Command_FlipCard_ext } from 'generated/proto/command_flip_card_pb'; import { FlipCardParams } from 'types'; export function flipCard(gameId: number, params: FlipCardParams): void { - BackendService.sendGameCommand(gameId, 'Command_FlipCard', params); + BackendService.sendGameCommand(gameId, Command_FlipCard_ext, create(Command_FlipCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/gameCommands.spec.ts b/webclient/src/websocket/commands/game/gameCommands.spec.ts index 023b5da75..5ac94fc9b 100644 --- a/webclient/src/websocket/commands/game/gameCommands.spec.ts +++ b/webclient/src/websocket/commands/game/gameCommands.spec.ts @@ -1,4 +1,37 @@ import { BackendService } from '../../services/BackendService'; +import { create, setExtension } from '@bufbuild/protobuf'; +import { GameCommandSchema, Command_Judge_ext } from 'generated/proto/game_commands_pb'; +import { Command_DrawCardsSchema, Command_DrawCards_ext } from 'generated/proto/command_draw_cards_pb'; +import { Command_AttachCard_ext } from 'generated/proto/command_attach_card_pb'; +import { Command_ChangeZoneProperties_ext } from 'generated/proto/command_change_zone_properties_pb'; +import { Command_Concede_ext, Command_Unconcede_ext } from 'generated/proto/command_concede_pb'; +import { Command_CreateArrow_ext } from 'generated/proto/command_create_arrow_pb'; +import { Command_CreateCounter_ext } from 'generated/proto/command_create_counter_pb'; +import { Command_CreateToken_ext } from 'generated/proto/command_create_token_pb'; +import { Command_DeckSelect_ext } from 'generated/proto/command_deck_select_pb'; +import { Command_DelCounter_ext } from 'generated/proto/command_del_counter_pb'; +import { Command_DeleteArrow_ext } from 'generated/proto/command_delete_arrow_pb'; +import { Command_DumpZone_ext } from 'generated/proto/command_dump_zone_pb'; +import { Command_FlipCard_ext } from 'generated/proto/command_flip_card_pb'; +import { Command_GameSay_ext } from 'generated/proto/command_game_say_pb'; +import { Command_IncCardCounter_ext } from 'generated/proto/command_inc_card_counter_pb'; +import { Command_IncCounter_ext } from 'generated/proto/command_inc_counter_pb'; +import { Command_KickFromGame_ext } from 'generated/proto/command_kick_from_game_pb'; +import { Command_LeaveGame_ext } from 'generated/proto/command_leave_game_pb'; +import { Command_MoveCard_ext } from 'generated/proto/command_move_card_pb'; +import { Command_Mulligan_ext } from 'generated/proto/command_mulligan_pb'; +import { Command_NextTurn_ext } from 'generated/proto/command_next_turn_pb'; +import { Command_ReadyStart_ext } from 'generated/proto/command_ready_start_pb'; +import { Command_RevealCards_ext } from 'generated/proto/command_reveal_cards_pb'; +import { Command_ReverseTurn_ext } from 'generated/proto/command_reverse_turn_pb'; +import { Command_SetActivePhase_ext } from 'generated/proto/command_set_active_phase_pb'; +import { Command_SetCardAttr_ext } from 'generated/proto/command_set_card_attr_pb'; +import { Command_SetCardCounter_ext } from 'generated/proto/command_set_card_counter_pb'; +import { Command_SetCounter_ext } from 'generated/proto/command_set_counter_pb'; +import { Command_SetSideboardLock_ext } from 'generated/proto/command_set_sideboard_lock_pb'; +import { Command_SetSideboardPlan_ext } from 'generated/proto/command_set_sideboard_plan_pb'; +import { Command_Shuffle_ext } from 'generated/proto/command_shuffle_pb'; +import { Command_UndoDraw_ext } from 'generated/proto/command_undo_draw_pb'; import { attachCard } from './attachCard'; import { changeZoneProperties } from './changeZoneProperties'; import { concede } from './concede'; @@ -38,7 +71,6 @@ vi.mock('../../services/BackendService', () => ({ })); const gameId = 1; -const params = {} as any; beforeEach(() => { (BackendService.sendGameCommand as vi.Mock).mockClear(); @@ -46,172 +78,208 @@ beforeEach(() => { describe('Game commands — delegate to BackendService.sendGameCommand', () => { it('attachCard sends Command_AttachCard', () => { - attachCard(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_AttachCard', params); + attachCard(gameId, { cardId: 10, startZone: 'hand' }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_AttachCard_ext, expect.objectContaining({ cardId: 10, startZone: 'hand' }) + ); }); it('changeZoneProperties sends Command_ChangeZoneProperties', () => { - changeZoneProperties(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_ChangeZoneProperties', params); + changeZoneProperties(gameId, { zoneName: 'side' }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_ChangeZoneProperties_ext, expect.objectContaining({ zoneName: 'side' }) + ); }); it('concede sends Command_Concede with empty object', () => { concede(gameId); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_Concede', {}); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Concede_ext, expect.any(Object)); }); it('createArrow sends Command_CreateArrow', () => { - createArrow(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_CreateArrow', params); + createArrow(gameId, { startPlayerId: 1, startZone: 'hand' }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_CreateArrow_ext, expect.objectContaining({ startPlayerId: 1, startZone: 'hand' }) + ); }); it('createCounter sends Command_CreateCounter', () => { - createCounter(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_CreateCounter', params); + createCounter(gameId, { counterName: 'life' }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_CreateCounter_ext, expect.objectContaining({ counterName: 'life' }) + ); }); it('createToken sends Command_CreateToken', () => { - createToken(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_CreateToken', params); + createToken(gameId, { cardName: 'Goblin', zone: 'play' }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_CreateToken_ext, expect.objectContaining({ cardName: 'Goblin', zone: 'play' }) + ); }); it('deckSelect sends Command_DeckSelect', () => { - deckSelect(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_DeckSelect', params); + deckSelect(gameId, { deckId: 5 }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_DeckSelect_ext, expect.objectContaining({ deckId: 5 })); }); it('delCounter sends Command_DelCounter', () => { - delCounter(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_DelCounter', params); + delCounter(gameId, { counterId: 3 }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_DelCounter_ext, expect.objectContaining({ counterId: 3 })); }); it('deleteArrow sends Command_DeleteArrow', () => { - deleteArrow(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_DeleteArrow', params); + deleteArrow(gameId, { arrowId: 2 }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_DeleteArrow_ext, expect.objectContaining({ arrowId: 2 })); }); it('drawCards sends Command_DrawCards', () => { - drawCards(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_DrawCards', params); + drawCards(gameId, { number: 3 }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_DrawCards_ext, expect.objectContaining({ number: 3 })); }); it('dumpZone sends Command_DumpZone', () => { - dumpZone(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_DumpZone', params); + dumpZone(gameId, { playerId: 2, zoneName: 'library' }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_DumpZone_ext, expect.objectContaining({ playerId: 2, zoneName: 'library' }) + ); }); it('flipCard sends Command_FlipCard', () => { - flipCard(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_FlipCard', params); + flipCard(gameId, { cardId: 7, faceDown: false }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_FlipCard_ext, expect.objectContaining({ cardId: 7, faceDown: false }) + ); }); it('gameSay sends Command_GameSay', () => { - gameSay(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_GameSay', params); + gameSay(gameId, { message: 'hello' }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_GameSay_ext, expect.objectContaining({ message: 'hello' })); }); it('incCardCounter sends Command_IncCardCounter', () => { - incCardCounter(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_IncCardCounter', params); + incCardCounter(gameId, { cardId: 5, counterId: 1 }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_IncCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) + ); }); it('incCounter sends Command_IncCounter', () => { - incCounter(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_IncCounter', params); + incCounter(gameId, { counterId: 1, delta: 5 }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_IncCounter_ext, expect.objectContaining({ counterId: 1, delta: 5 }) + ); }); it('kickFromGame sends Command_KickFromGame', () => { - kickFromGame(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_KickFromGame', params); + kickFromGame(gameId, { playerId: 2 }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_KickFromGame_ext, expect.objectContaining({ playerId: 2 })); }); it('leaveGame sends Command_LeaveGame with empty object', () => { leaveGame(gameId); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_LeaveGame', {}); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_LeaveGame_ext, expect.any(Object)); }); it('moveCard sends Command_MoveCard', () => { - moveCard(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_MoveCard', params); + moveCard(gameId, { startZone: 'hand', targetZone: 'graveyard' }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_MoveCard_ext, + expect.objectContaining({ startZone: 'hand', targetZone: 'graveyard' }) + ); }); it('mulligan sends Command_Mulligan', () => { - mulligan(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_Mulligan', params); + mulligan(gameId, { number: 7 }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Mulligan_ext, expect.objectContaining({ number: 7 })); }); it('nextTurn sends Command_NextTurn with empty object', () => { nextTurn(gameId); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_NextTurn', {}); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_NextTurn_ext, expect.any(Object)); }); it('readyStart sends Command_ReadyStart', () => { - readyStart(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_ReadyStart', params); + readyStart(gameId, { ready: true }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_ReadyStart_ext, expect.objectContaining({ ready: true })); }); it('revealCards sends Command_RevealCards', () => { - revealCards(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_RevealCards', params); + revealCards(gameId, { zoneName: 'hand', cardId: [1, 2] }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_RevealCards_ext, expect.objectContaining({ zoneName: 'hand', cardId: [1, 2] }) + ); }); it('reverseTurn sends Command_ReverseTurn with empty object', () => { reverseTurn(gameId); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_ReverseTurn', {}); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_ReverseTurn_ext, expect.any(Object)); }); it('setActivePhase sends Command_SetActivePhase', () => { - setActivePhase(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetActivePhase', params); + setActivePhase(gameId, { phase: 2 }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_SetActivePhase_ext, expect.objectContaining({ phase: 2 })); }); it('setCardAttr sends Command_SetCardAttr', () => { - setCardAttr(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetCardAttr', params); + setCardAttr(gameId, { zone: 'play', cardId: 5, attrValue: '2' }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_SetCardAttr_ext, + expect.objectContaining({ zone: 'play', cardId: 5, attrValue: '2' }) + ); }); it('setCardCounter sends Command_SetCardCounter', () => { - setCardCounter(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetCardCounter', params); + setCardCounter(gameId, { cardId: 5, counterId: 1 }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_SetCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) + ); }); it('setCounter sends Command_SetCounter', () => { - setCounter(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetCounter', params); + setCounter(gameId, { counterId: 1, value: 10 }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_SetCounter_ext, expect.objectContaining({ counterId: 1, value: 10 }) + ); }); it('setSideboardLock sends Command_SetSideboardLock', () => { - setSideboardLock(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetSideboardLock', params); + setSideboardLock(gameId, { locked: true }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_SetSideboardLock_ext, expect.objectContaining({ locked: true }) + ); }); it('setSideboardPlan sends Command_SetSideboardPlan', () => { - setSideboardPlan(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_SetSideboardPlan', params); + setSideboardPlan(gameId, { moveList: [] }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, Command_SetSideboardPlan_ext, expect.objectContaining({ moveList: expect.any(Array) }) + ); }); it('shuffle sends Command_Shuffle', () => { - shuffle(gameId, params); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_Shuffle', params); + shuffle(gameId, { zoneName: 'hand' }); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Shuffle_ext, expect.objectContaining({ zoneName: 'hand' })); }); it('undoDraw sends Command_UndoDraw with empty object', () => { undoDraw(gameId); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_UndoDraw', {}); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_UndoDraw_ext, expect.any(Object)); }); it('unconcede sends Command_Unconcede with empty object', () => { unconcede(gameId); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_Unconcede', {}); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Unconcede_ext, expect.any(Object)); }); it('judge sends Command_Judge with targetId and wrapped gameCommand array', () => { const targetId = 3; - const innerGameCommand = { '.Command_DrawCards.ext': { numberOfCards: 2 } }; - judge(gameId, targetId, innerGameCommand); - expect(BackendService.sendGameCommand).toHaveBeenCalledWith(gameId, 'Command_Judge', { - targetId, - gameCommand: [innerGameCommand], - }); + const innerCmd = create(GameCommandSchema); + setExtension(innerCmd, Command_DrawCards_ext, create(Command_DrawCardsSchema, { number: 2 })); + judge(gameId, targetId, innerCmd); + expect(BackendService.sendGameCommand).toHaveBeenCalledWith( + gameId, + Command_Judge_ext, + expect.objectContaining({ targetId: 3, gameCommand: expect.any(Array) }) + ); }); }); diff --git a/webclient/src/websocket/commands/game/gameSay.ts b/webclient/src/websocket/commands/game/gameSay.ts index 2f574b52e..a55085526 100644 --- a/webclient/src/websocket/commands/game/gameSay.ts +++ b/webclient/src/websocket/commands/game/gameSay.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_GameSaySchema, Command_GameSay_ext } from 'generated/proto/command_game_say_pb'; import { GameSayParams } from 'types'; export function gameSay(gameId: number, params: GameSayParams): void { - BackendService.sendGameCommand(gameId, 'Command_GameSay', params); + BackendService.sendGameCommand(gameId, Command_GameSay_ext, create(Command_GameSaySchema, params)); } diff --git a/webclient/src/websocket/commands/game/incCardCounter.ts b/webclient/src/websocket/commands/game/incCardCounter.ts index e2fda829a..dd678044f 100644 --- a/webclient/src/websocket/commands/game/incCardCounter.ts +++ b/webclient/src/websocket/commands/game/incCardCounter.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_IncCardCounterSchema, Command_IncCardCounter_ext } from 'generated/proto/command_inc_card_counter_pb'; import { IncCardCounterParams } from 'types'; export function incCardCounter(gameId: number, params: IncCardCounterParams): void { - BackendService.sendGameCommand(gameId, 'Command_IncCardCounter', params); + BackendService.sendGameCommand(gameId, Command_IncCardCounter_ext, create(Command_IncCardCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/incCounter.ts b/webclient/src/websocket/commands/game/incCounter.ts index a24173679..997fc1303 100644 --- a/webclient/src/websocket/commands/game/incCounter.ts +++ b/webclient/src/websocket/commands/game/incCounter.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_IncCounterSchema, Command_IncCounter_ext } from 'generated/proto/command_inc_counter_pb'; import { IncCounterParams } from 'types'; export function incCounter(gameId: number, params: IncCounterParams): void { - BackendService.sendGameCommand(gameId, 'Command_IncCounter', params); + BackendService.sendGameCommand(gameId, Command_IncCounter_ext, create(Command_IncCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/judge.ts b/webclient/src/websocket/commands/game/judge.ts index 1b9f22d14..57ad869eb 100644 --- a/webclient/src/websocket/commands/game/judge.ts +++ b/webclient/src/websocket/commands/game/judge.ts @@ -1,8 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_JudgeSchema, Command_Judge_ext } from 'generated/proto/game_commands_pb'; +import type { GameCommand } from 'generated/proto/game_commands_pb'; -export function judge(gameId: number, targetId: number, innerGameCommand: any): void { - BackendService.sendGameCommand(gameId, 'Command_Judge', { +export function judge(gameId: number, targetId: number, innerGameCommand: GameCommand): void { + BackendService.sendGameCommand(gameId, Command_Judge_ext, create(Command_JudgeSchema, { targetId, gameCommand: [innerGameCommand], - }); + })); } + diff --git a/webclient/src/websocket/commands/game/kickFromGame.ts b/webclient/src/websocket/commands/game/kickFromGame.ts index c14aab980..e56574e41 100644 --- a/webclient/src/websocket/commands/game/kickFromGame.ts +++ b/webclient/src/websocket/commands/game/kickFromGame.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_KickFromGameSchema, Command_KickFromGame_ext } from 'generated/proto/command_kick_from_game_pb'; import { KickFromGameParams } from 'types'; export function kickFromGame(gameId: number, params: KickFromGameParams): void { - BackendService.sendGameCommand(gameId, 'Command_KickFromGame', params); + BackendService.sendGameCommand(gameId, Command_KickFromGame_ext, create(Command_KickFromGameSchema, params)); } diff --git a/webclient/src/websocket/commands/game/leaveGame.ts b/webclient/src/websocket/commands/game/leaveGame.ts index 48c8dcc2a..6bdcfc661 100644 --- a/webclient/src/websocket/commands/game/leaveGame.ts +++ b/webclient/src/websocket/commands/game/leaveGame.ts @@ -1,5 +1,7 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_LeaveGameSchema, Command_LeaveGame_ext } from 'generated/proto/command_leave_game_pb'; export function leaveGame(gameId: number): void { - BackendService.sendGameCommand(gameId, 'Command_LeaveGame', {}); + BackendService.sendGameCommand(gameId, Command_LeaveGame_ext, create(Command_LeaveGameSchema)); } diff --git a/webclient/src/websocket/commands/game/moveCard.ts b/webclient/src/websocket/commands/game/moveCard.ts index aebc97bbd..193d85b93 100644 --- a/webclient/src/websocket/commands/game/moveCard.ts +++ b/webclient/src/websocket/commands/game/moveCard.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_MoveCardSchema, Command_MoveCard_ext } from 'generated/proto/command_move_card_pb'; import { MoveCardParams } from 'types'; export function moveCard(gameId: number, params: MoveCardParams): void { - BackendService.sendGameCommand(gameId, 'Command_MoveCard', params); + BackendService.sendGameCommand(gameId, Command_MoveCard_ext, create(Command_MoveCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/mulligan.ts b/webclient/src/websocket/commands/game/mulligan.ts index 9871acf27..54c42635e 100644 --- a/webclient/src/websocket/commands/game/mulligan.ts +++ b/webclient/src/websocket/commands/game/mulligan.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_MulliganSchema, Command_Mulligan_ext } from 'generated/proto/command_mulligan_pb'; import { MulliganParams } from 'types'; export function mulligan(gameId: number, params: MulliganParams): void { - BackendService.sendGameCommand(gameId, 'Command_Mulligan', params); + BackendService.sendGameCommand(gameId, Command_Mulligan_ext, create(Command_MulliganSchema, params)); } diff --git a/webclient/src/websocket/commands/game/nextTurn.ts b/webclient/src/websocket/commands/game/nextTurn.ts index 696002256..756cca49a 100644 --- a/webclient/src/websocket/commands/game/nextTurn.ts +++ b/webclient/src/websocket/commands/game/nextTurn.ts @@ -1,5 +1,7 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_NextTurnSchema, Command_NextTurn_ext } from 'generated/proto/command_next_turn_pb'; export function nextTurn(gameId: number): void { - BackendService.sendGameCommand(gameId, 'Command_NextTurn', {}); + BackendService.sendGameCommand(gameId, Command_NextTurn_ext, create(Command_NextTurnSchema)); } diff --git a/webclient/src/websocket/commands/game/readyStart.ts b/webclient/src/websocket/commands/game/readyStart.ts index 50e818914..d6a608d9c 100644 --- a/webclient/src/websocket/commands/game/readyStart.ts +++ b/webclient/src/websocket/commands/game/readyStart.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ReadyStartSchema, Command_ReadyStart_ext } from 'generated/proto/command_ready_start_pb'; import { ReadyStartParams } from 'types'; export function readyStart(gameId: number, params: ReadyStartParams): void { - BackendService.sendGameCommand(gameId, 'Command_ReadyStart', params); + BackendService.sendGameCommand(gameId, Command_ReadyStart_ext, create(Command_ReadyStartSchema, params)); } diff --git a/webclient/src/websocket/commands/game/revealCards.ts b/webclient/src/websocket/commands/game/revealCards.ts index 567970db5..18e054613 100644 --- a/webclient/src/websocket/commands/game/revealCards.ts +++ b/webclient/src/websocket/commands/game/revealCards.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_RevealCardsSchema, Command_RevealCards_ext } from 'generated/proto/command_reveal_cards_pb'; import { RevealCardsParams } from 'types'; export function revealCards(gameId: number, params: RevealCardsParams): void { - BackendService.sendGameCommand(gameId, 'Command_RevealCards', params); + BackendService.sendGameCommand(gameId, Command_RevealCards_ext, create(Command_RevealCardsSchema, params)); } diff --git a/webclient/src/websocket/commands/game/reverseTurn.ts b/webclient/src/websocket/commands/game/reverseTurn.ts index 27662ea7e..38d34600a 100644 --- a/webclient/src/websocket/commands/game/reverseTurn.ts +++ b/webclient/src/websocket/commands/game/reverseTurn.ts @@ -1,5 +1,7 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ReverseTurnSchema, Command_ReverseTurn_ext } from 'generated/proto/command_reverse_turn_pb'; export function reverseTurn(gameId: number): void { - BackendService.sendGameCommand(gameId, 'Command_ReverseTurn', {}); + BackendService.sendGameCommand(gameId, Command_ReverseTurn_ext, create(Command_ReverseTurnSchema)); } diff --git a/webclient/src/websocket/commands/game/setActivePhase.ts b/webclient/src/websocket/commands/game/setActivePhase.ts index 664815f16..799a700f3 100644 --- a/webclient/src/websocket/commands/game/setActivePhase.ts +++ b/webclient/src/websocket/commands/game/setActivePhase.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_SetActivePhaseSchema, Command_SetActivePhase_ext } from 'generated/proto/command_set_active_phase_pb'; import { SetActivePhaseParams } from 'types'; export function setActivePhase(gameId: number, params: SetActivePhaseParams): void { - BackendService.sendGameCommand(gameId, 'Command_SetActivePhase', params); + BackendService.sendGameCommand(gameId, Command_SetActivePhase_ext, create(Command_SetActivePhaseSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCardAttr.ts b/webclient/src/websocket/commands/game/setCardAttr.ts index 3d4418aa7..f1b2bbd56 100644 --- a/webclient/src/websocket/commands/game/setCardAttr.ts +++ b/webclient/src/websocket/commands/game/setCardAttr.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_SetCardAttrSchema, Command_SetCardAttr_ext } from 'generated/proto/command_set_card_attr_pb'; import { SetCardAttrParams } from 'types'; export function setCardAttr(gameId: number, params: SetCardAttrParams): void { - BackendService.sendGameCommand(gameId, 'Command_SetCardAttr', params); + BackendService.sendGameCommand(gameId, Command_SetCardAttr_ext, create(Command_SetCardAttrSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCardCounter.ts b/webclient/src/websocket/commands/game/setCardCounter.ts index 87e1940d2..2aea270f8 100644 --- a/webclient/src/websocket/commands/game/setCardCounter.ts +++ b/webclient/src/websocket/commands/game/setCardCounter.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_SetCardCounterSchema, Command_SetCardCounter_ext } from 'generated/proto/command_set_card_counter_pb'; import { SetCardCounterParams } from 'types'; export function setCardCounter(gameId: number, params: SetCardCounterParams): void { - BackendService.sendGameCommand(gameId, 'Command_SetCardCounter', params); + BackendService.sendGameCommand(gameId, Command_SetCardCounter_ext, create(Command_SetCardCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCounter.ts b/webclient/src/websocket/commands/game/setCounter.ts index a850d5111..fdbb90bf9 100644 --- a/webclient/src/websocket/commands/game/setCounter.ts +++ b/webclient/src/websocket/commands/game/setCounter.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_SetCounterSchema, Command_SetCounter_ext } from 'generated/proto/command_set_counter_pb'; import { SetCounterParams } from 'types'; export function setCounter(gameId: number, params: SetCounterParams): void { - BackendService.sendGameCommand(gameId, 'Command_SetCounter', params); + BackendService.sendGameCommand(gameId, Command_SetCounter_ext, create(Command_SetCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setSideboardLock.ts b/webclient/src/websocket/commands/game/setSideboardLock.ts index 00df8c763..a8b82dff9 100644 --- a/webclient/src/websocket/commands/game/setSideboardLock.ts +++ b/webclient/src/websocket/commands/game/setSideboardLock.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_SetSideboardLockSchema, Command_SetSideboardLock_ext } from 'generated/proto/command_set_sideboard_lock_pb'; import { SetSideboardLockParams } from 'types'; export function setSideboardLock(gameId: number, params: SetSideboardLockParams): void { - BackendService.sendGameCommand(gameId, 'Command_SetSideboardLock', params); + BackendService.sendGameCommand(gameId, Command_SetSideboardLock_ext, create(Command_SetSideboardLockSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setSideboardPlan.ts b/webclient/src/websocket/commands/game/setSideboardPlan.ts index ad7278738..65cac24e6 100644 --- a/webclient/src/websocket/commands/game/setSideboardPlan.ts +++ b/webclient/src/websocket/commands/game/setSideboardPlan.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_SetSideboardPlanSchema, Command_SetSideboardPlan_ext } from 'generated/proto/command_set_sideboard_plan_pb'; import { SetSideboardPlanParams } from 'types'; export function setSideboardPlan(gameId: number, params: SetSideboardPlanParams): void { - BackendService.sendGameCommand(gameId, 'Command_SetSideboardPlan', params); + BackendService.sendGameCommand(gameId, Command_SetSideboardPlan_ext, create(Command_SetSideboardPlanSchema, params)); } diff --git a/webclient/src/websocket/commands/game/shuffle.ts b/webclient/src/websocket/commands/game/shuffle.ts index 777d03fd6..750926d3b 100644 --- a/webclient/src/websocket/commands/game/shuffle.ts +++ b/webclient/src/websocket/commands/game/shuffle.ts @@ -1,6 +1,8 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ShuffleSchema, Command_Shuffle_ext } from 'generated/proto/command_shuffle_pb'; import { ShuffleParams } from 'types'; export function shuffle(gameId: number, params: ShuffleParams): void { - BackendService.sendGameCommand(gameId, 'Command_Shuffle', params); + BackendService.sendGameCommand(gameId, Command_Shuffle_ext, create(Command_ShuffleSchema, params)); } diff --git a/webclient/src/websocket/commands/game/unconcede.ts b/webclient/src/websocket/commands/game/unconcede.ts index b724aee03..82cc0d63f 100644 --- a/webclient/src/websocket/commands/game/unconcede.ts +++ b/webclient/src/websocket/commands/game/unconcede.ts @@ -1,5 +1,7 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_UnconcedeSchema, Command_Unconcede_ext } from 'generated/proto/command_concede_pb'; export function unconcede(gameId: number): void { - BackendService.sendGameCommand(gameId, 'Command_Unconcede', {}); + BackendService.sendGameCommand(gameId, Command_Unconcede_ext, create(Command_UnconcedeSchema)); } diff --git a/webclient/src/websocket/commands/game/undoDraw.ts b/webclient/src/websocket/commands/game/undoDraw.ts index 28d47c093..9f70df3b8 100644 --- a/webclient/src/websocket/commands/game/undoDraw.ts +++ b/webclient/src/websocket/commands/game/undoDraw.ts @@ -1,5 +1,7 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_UndoDrawSchema, Command_UndoDraw_ext } from 'generated/proto/command_undo_draw_pb'; export function undoDraw(gameId: number): void { - BackendService.sendGameCommand(gameId, 'Command_UndoDraw', {}); + BackendService.sendGameCommand(gameId, Command_UndoDraw_ext, create(Command_UndoDrawSchema)); } diff --git a/webclient/src/websocket/commands/moderator/banFromServer.ts b/webclient/src/websocket/commands/moderator/banFromServer.ts index e45e34504..9fba4e72b 100644 --- a/webclient/src/websocket/commands/moderator/banFromServer.ts +++ b/webclient/src/websocket/commands/moderator/banFromServer.ts @@ -1,11 +1,13 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_BanFromServer_ext, Command_BanFromServerSchema } from 'generated/proto/moderator_commands_pb'; import { ModeratorPersistence } from '../../persistence'; export function banFromServer(minutes: number, userName?: string, address?: string, reason?: string, visibleReason?: string, clientid?: string, removeMessages?: number): void { - BackendService.sendModeratorCommand('Command_BanFromServer', { + BackendService.sendModeratorCommand(Command_BanFromServer_ext, create(Command_BanFromServerSchema, { minutes, userName, address, reason, visibleReason, clientid, removeMessages - }, { + }), { onSuccess: () => { ModeratorPersistence.banFromServer(userName); }, diff --git a/webclient/src/websocket/commands/moderator/forceActivateUser.ts b/webclient/src/websocket/commands/moderator/forceActivateUser.ts index d4138a015..43dd1fc65 100644 --- a/webclient/src/websocket/commands/moderator/forceActivateUser.ts +++ b/webclient/src/websocket/commands/moderator/forceActivateUser.ts @@ -1,8 +1,13 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { + Command_ForceActivateUser_ext, Command_ForceActivateUserSchema, +} from 'generated/proto/moderator_commands_pb'; import { ModeratorPersistence } from '../../persistence'; export function forceActivateUser(usernameToActivate: string, moderatorName: string): void { - BackendService.sendModeratorCommand('Command_ForceActivateUser', { usernameToActivate, moderatorName }, { + const cmd = create(Command_ForceActivateUserSchema, { usernameToActivate, moderatorName }); + BackendService.sendModeratorCommand(Command_ForceActivateUser_ext, cmd, { onSuccess: () => { ModeratorPersistence.forceActivateUser(usernameToActivate, moderatorName); }, diff --git a/webclient/src/websocket/commands/moderator/getAdminNotes.ts b/webclient/src/websocket/commands/moderator/getAdminNotes.ts index d4f626aa2..48b374581 100644 --- a/webclient/src/websocket/commands/moderator/getAdminNotes.ts +++ b/webclient/src/websocket/commands/moderator/getAdminNotes.ts @@ -1,9 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_GetAdminNotes_ext, Command_GetAdminNotesSchema } from 'generated/proto/moderator_commands_pb'; import { ModeratorPersistence } from '../../persistence'; +import { Response_GetAdminNotes_ext } from 'generated/proto/response_get_admin_notes_pb'; export function getAdminNotes(userName: string): void { - BackendService.sendModeratorCommand('Command_GetAdminNotes', { userName }, { - responseName: 'Response_GetAdminNotes', + BackendService.sendModeratorCommand(Command_GetAdminNotes_ext, create(Command_GetAdminNotesSchema, { userName }), { + responseExt: Response_GetAdminNotes_ext, onSuccess: (response) => { ModeratorPersistence.getAdminNotes(userName, response.notes); }, diff --git a/webclient/src/websocket/commands/moderator/getBanHistory.ts b/webclient/src/websocket/commands/moderator/getBanHistory.ts index dd4e90eda..a6a084912 100644 --- a/webclient/src/websocket/commands/moderator/getBanHistory.ts +++ b/webclient/src/websocket/commands/moderator/getBanHistory.ts @@ -1,9 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_GetBanHistory_ext, Command_GetBanHistorySchema } from 'generated/proto/moderator_commands_pb'; import { ModeratorPersistence } from '../../persistence'; +import { Response_BanHistory_ext } from 'generated/proto/response_ban_history_pb'; export function getBanHistory(userName: string): void { - BackendService.sendModeratorCommand('Command_GetBanHistory', { userName }, { - responseName: 'Response_BanHistory', + BackendService.sendModeratorCommand(Command_GetBanHistory_ext, create(Command_GetBanHistorySchema, { userName }), { + responseExt: Response_BanHistory_ext, onSuccess: (response) => { ModeratorPersistence.banHistory(userName, response.banList); }, diff --git a/webclient/src/websocket/commands/moderator/getWarnHistory.ts b/webclient/src/websocket/commands/moderator/getWarnHistory.ts index c47e2c6e4..15b03595e 100644 --- a/webclient/src/websocket/commands/moderator/getWarnHistory.ts +++ b/webclient/src/websocket/commands/moderator/getWarnHistory.ts @@ -1,9 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_GetWarnHistory_ext, Command_GetWarnHistorySchema } from 'generated/proto/moderator_commands_pb'; import { ModeratorPersistence } from '../../persistence'; +import { Response_WarnHistory_ext } from 'generated/proto/response_warn_history_pb'; export function getWarnHistory(userName: string): void { - BackendService.sendModeratorCommand('Command_GetWarnHistory', { userName }, { - responseName: 'Response_WarnHistory', + BackendService.sendModeratorCommand(Command_GetWarnHistory_ext, create(Command_GetWarnHistorySchema, { userName }), { + responseExt: Response_WarnHistory_ext, onSuccess: (response) => { ModeratorPersistence.warnHistory(userName, response.warnList); }, diff --git a/webclient/src/websocket/commands/moderator/getWarnList.ts b/webclient/src/websocket/commands/moderator/getWarnList.ts index 412aee09e..3f7e178ed 100644 --- a/webclient/src/websocket/commands/moderator/getWarnList.ts +++ b/webclient/src/websocket/commands/moderator/getWarnList.ts @@ -1,9 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_GetWarnList_ext, Command_GetWarnListSchema } from 'generated/proto/moderator_commands_pb'; import { ModeratorPersistence } from '../../persistence'; +import { Response_WarnList_ext } from 'generated/proto/response_warn_list_pb'; export function getWarnList(modName: string, userName: string, userClientid: string): void { - BackendService.sendModeratorCommand('Command_GetWarnList', { modName, userName, userClientid }, { - responseName: 'Response_WarnList', + BackendService.sendModeratorCommand(Command_GetWarnList_ext, create(Command_GetWarnListSchema, { modName, userName, userClientid }), { + responseExt: Response_WarnList_ext, onSuccess: (response) => { ModeratorPersistence.warnListOptions(response.warning); }, diff --git a/webclient/src/websocket/commands/moderator/grantReplayAccess.ts b/webclient/src/websocket/commands/moderator/grantReplayAccess.ts index 74d64d17a..84adc725e 100644 --- a/webclient/src/websocket/commands/moderator/grantReplayAccess.ts +++ b/webclient/src/websocket/commands/moderator/grantReplayAccess.ts @@ -1,8 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { + Command_GrantReplayAccess_ext, Command_GrantReplayAccessSchema, +} from 'generated/proto/moderator_commands_pb'; import { ModeratorPersistence } from '../../persistence'; export function grantReplayAccess(replayId: number, moderatorName: string): void { - BackendService.sendModeratorCommand('Command_GrantReplayAccess', { replayId, moderatorName }, { + BackendService.sendModeratorCommand(Command_GrantReplayAccess_ext, create(Command_GrantReplayAccessSchema, { replayId, moderatorName }), { onSuccess: () => { ModeratorPersistence.grantReplayAccess(replayId, moderatorName); }, diff --git a/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts b/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts index 47dedbfaa..576983942 100644 --- a/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts +++ b/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts @@ -22,6 +22,23 @@ vi.mock('../../persistence', () => ({ import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; import { BackendService } from '../../services/BackendService'; import { ModeratorPersistence } from '../../persistence'; +import { + Command_BanFromServer_ext, + Command_ForceActivateUser_ext, + Command_GetAdminNotes_ext, + Command_GetBanHistory_ext, + Command_GetWarnHistory_ext, + Command_GetWarnList_ext, + Command_GrantReplayAccess_ext, + Command_UpdateAdminNotes_ext, + Command_ViewLogHistory_ext, + Command_WarnUser_ext, +} from 'generated/proto/moderator_commands_pb'; +import { Response_GetAdminNotes_ext } from 'generated/proto/response_get_admin_notes_pb'; +import { Response_BanHistory_ext } from 'generated/proto/response_ban_history_pb'; +import { Response_WarnHistory_ext } from 'generated/proto/response_warn_history_pb'; +import { Response_WarnList_ext } from 'generated/proto/response_warn_list_pb'; +import { Response_ViewLogHistory_ext } from 'generated/proto/response_viewlog_history_pb'; import { banFromServer } from './banFromServer'; import { forceActivateUser } from './forceActivateUser'; import { getAdminNotes } from './getAdminNotes'; @@ -34,7 +51,8 @@ import { viewLogHistory } from './viewLogHistory'; import { warnUser } from './warnUser'; const { getLastSendOpts, invokeOnSuccess } = makeCallbackHelpers( - BackendService.sendModeratorCommand as vi.Mock + BackendService.sendModeratorCommand as vi.Mock, + 2 ); beforeEach(() => vi.clearAllMocks()); @@ -47,7 +65,7 @@ describe('banFromServer', () => { it('calls sendModeratorCommand with Command_BanFromServer', () => { banFromServer(30, 'alice', '1.2.3.4', 'reason', 'visible', 'cid', 1); expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith( - 'Command_BanFromServer', + Command_BanFromServer_ext, expect.objectContaining({ minutes: 30, userName: 'alice' }), expect.any(Object) ); @@ -67,7 +85,7 @@ describe('forceActivateUser', () => { it('calls sendModeratorCommand with Command_ForceActivateUser', () => { forceActivateUser('alice', 'mod1'); - expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith('Command_ForceActivateUser', expect.any(Object), expect.any(Object)); + expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith(Command_ForceActivateUser_ext, expect.any(Object), expect.any(Object)); }); it('onSuccess calls ModeratorPersistence.forceActivateUser', () => { @@ -85,16 +103,16 @@ describe('getAdminNotes', () => { it('calls sendModeratorCommand with Command_GetAdminNotes', () => { getAdminNotes('alice'); expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith( - 'Command_GetAdminNotes', + Command_GetAdminNotes_ext, expect.any(Object), - expect.objectContaining({ responseName: 'Response_GetAdminNotes' }) + expect.objectContaining({ responseExt: Response_GetAdminNotes_ext }) ); }); it('onSuccess calls ModeratorPersistence.getAdminNotes with notes', () => { getAdminNotes('alice'); const resp = { notes: 'some notes' }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_GetAdminNotes.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(ModeratorPersistence.getAdminNotes).toHaveBeenCalledWith('alice', 'some notes'); }); }); @@ -107,16 +125,16 @@ describe('getBanHistory', () => { it('calls sendModeratorCommand with Command_GetBanHistory', () => { getBanHistory('alice'); expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith( - 'Command_GetBanHistory', + Command_GetBanHistory_ext, expect.any(Object), - expect.objectContaining({ responseName: 'Response_BanHistory' }) + expect.objectContaining({ responseExt: Response_BanHistory_ext }) ); }); it('onSuccess calls ModeratorPersistence.banHistory with banList', () => { getBanHistory('alice'); const resp = { banList: [{ id: 1 }] }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_BanHistory.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(ModeratorPersistence.banHistory).toHaveBeenCalledWith('alice', [{ id: 1 }]); }); }); @@ -129,16 +147,16 @@ describe('getWarnHistory', () => { it('calls sendModeratorCommand with Command_GetWarnHistory', () => { getWarnHistory('alice'); expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith( - 'Command_GetWarnHistory', + Command_GetWarnHistory_ext, expect.any(Object), - expect.objectContaining({ responseName: 'Response_WarnHistory' }) + expect.objectContaining({ responseExt: Response_WarnHistory_ext }) ); }); it('onSuccess calls ModeratorPersistence.warnHistory with warnList', () => { getWarnHistory('alice'); const resp = { warnList: [{ id: 2 }] }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_WarnHistory.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(ModeratorPersistence.warnHistory).toHaveBeenCalledWith('alice', [{ id: 2 }]); }); }); @@ -151,16 +169,16 @@ describe('getWarnList', () => { it('calls sendModeratorCommand with Command_GetWarnList', () => { getWarnList('mod1', 'alice', 'US'); expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith( - 'Command_GetWarnList', + Command_GetWarnList_ext, expect.any(Object), - expect.objectContaining({ responseName: 'Response_WarnList' }) + expect.objectContaining({ responseExt: Response_WarnList_ext }) ); }); it('onSuccess calls ModeratorPersistence.warnListOptions with warning', () => { getWarnList('mod1', 'alice', 'US'); const resp = { warning: ['w1', 'w2'] }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_WarnList.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(ModeratorPersistence.warnListOptions).toHaveBeenCalledWith(['w1', 'w2']); }); }); @@ -172,7 +190,7 @@ describe('grantReplayAccess', () => { it('calls sendModeratorCommand with Command_GrantReplayAccess', () => { grantReplayAccess(10, 'mod1'); - expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith('Command_GrantReplayAccess', expect.any(Object), expect.any(Object)); + expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith(Command_GrantReplayAccess_ext, expect.any(Object), expect.any(Object)); }); it('onSuccess calls ModeratorPersistence.grantReplayAccess', () => { @@ -189,7 +207,7 @@ describe('updateAdminNotes', () => { it('calls sendModeratorCommand with Command_UpdateAdminNotes', () => { updateAdminNotes('alice', 'new notes'); - expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith('Command_UpdateAdminNotes', expect.any(Object), expect.any(Object)); + expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith(Command_UpdateAdminNotes_ext, expect.any(Object), expect.any(Object)); }); it('onSuccess calls ModeratorPersistence.updateAdminNotes', () => { @@ -205,18 +223,18 @@ describe('updateAdminNotes', () => { describe('viewLogHistory', () => { it('calls sendModeratorCommand with Command_ViewLogHistory', () => { - viewLogHistory({ filters: 'all' } as any); + viewLogHistory({ dateRange: 7 } as any); expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith( - 'Command_ViewLogHistory', + Command_ViewLogHistory_ext, expect.any(Object), - expect.objectContaining({ responseName: 'Response_ViewLogHistory' }) + expect.objectContaining({ responseExt: Response_ViewLogHistory_ext }) ); }); it('onSuccess calls ModeratorPersistence.viewLogs with logMessage', () => { - viewLogHistory({ filters: 'all' } as any); + viewLogHistory({ dateRange: 7 } as any); const resp = { logMessage: ['log1'] }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_ViewLogHistory.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(ModeratorPersistence.viewLogs).toHaveBeenCalledWith(['log1']); }); }); @@ -228,7 +246,7 @@ describe('warnUser', () => { it('calls sendModeratorCommand with Command_WarnUser', () => { warnUser('alice', 'bad behavior', 'cid'); - expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith('Command_WarnUser', expect.any(Object), expect.any(Object)); + expect(BackendService.sendModeratorCommand).toHaveBeenCalledWith(Command_WarnUser_ext, expect.any(Object), expect.any(Object)); }); it('onSuccess calls ModeratorPersistence.warnUser', () => { diff --git a/webclient/src/websocket/commands/moderator/updateAdminNotes.ts b/webclient/src/websocket/commands/moderator/updateAdminNotes.ts index c7ac315c5..6f50e71aa 100644 --- a/webclient/src/websocket/commands/moderator/updateAdminNotes.ts +++ b/webclient/src/websocket/commands/moderator/updateAdminNotes.ts @@ -1,8 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { + Command_UpdateAdminNotes_ext, Command_UpdateAdminNotesSchema, +} from 'generated/proto/moderator_commands_pb'; import { ModeratorPersistence } from '../../persistence'; export function updateAdminNotes(userName: string, notes: string): void { - BackendService.sendModeratorCommand('Command_UpdateAdminNotes', { userName, notes }, { + BackendService.sendModeratorCommand(Command_UpdateAdminNotes_ext, create(Command_UpdateAdminNotesSchema, { userName, notes }), { onSuccess: () => { ModeratorPersistence.updateAdminNotes(userName, notes); }, diff --git a/webclient/src/websocket/commands/moderator/viewLogHistory.ts b/webclient/src/websocket/commands/moderator/viewLogHistory.ts index 19a930608..82d101f99 100644 --- a/webclient/src/websocket/commands/moderator/viewLogHistory.ts +++ b/webclient/src/websocket/commands/moderator/viewLogHistory.ts @@ -1,10 +1,13 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ViewLogHistory_ext, Command_ViewLogHistorySchema } from 'generated/proto/moderator_commands_pb'; import { ModeratorPersistence } from '../../persistence'; +import { Response_ViewLogHistory_ext } from 'generated/proto/response_viewlog_history_pb'; import { LogFilters } from 'types'; export function viewLogHistory(filters: LogFilters): void { - BackendService.sendModeratorCommand('Command_ViewLogHistory', filters, { - responseName: 'Response_ViewLogHistory', + BackendService.sendModeratorCommand(Command_ViewLogHistory_ext, create(Command_ViewLogHistorySchema, filters), { + responseExt: Response_ViewLogHistory_ext, onSuccess: (response) => { ModeratorPersistence.viewLogs(response.logMessage); }, diff --git a/webclient/src/websocket/commands/moderator/warnUser.ts b/webclient/src/websocket/commands/moderator/warnUser.ts index 0e0271d4b..20cd7b565 100644 --- a/webclient/src/websocket/commands/moderator/warnUser.ts +++ b/webclient/src/websocket/commands/moderator/warnUser.ts @@ -1,8 +1,11 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_WarnUser_ext, Command_WarnUserSchema } from 'generated/proto/moderator_commands_pb'; import { ModeratorPersistence } from '../../persistence'; export function warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { - BackendService.sendModeratorCommand('Command_WarnUser', { userName, reason, clientid, removeMessages }, { + const cmd = create(Command_WarnUserSchema, { userName, reason, clientid, removeMessages }); + BackendService.sendModeratorCommand(Command_WarnUser_ext, cmd, { onSuccess: () => { ModeratorPersistence.warnUser(userName); }, diff --git a/webclient/src/websocket/commands/room/createGame.ts b/webclient/src/websocket/commands/room/createGame.ts index 62565e0e6..c49691657 100644 --- a/webclient/src/websocket/commands/room/createGame.ts +++ b/webclient/src/websocket/commands/room/createGame.ts @@ -1,9 +1,11 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_CreateGame_ext, Command_CreateGameSchema } from 'generated/proto/room_commands_pb'; import { RoomPersistence } from '../../persistence'; import { GameConfig } from 'types'; export function createGame(roomId: number, gameConfig: GameConfig): void { - BackendService.sendRoomCommand(roomId, 'Command_CreateGame', gameConfig, { + BackendService.sendRoomCommand(roomId, Command_CreateGame_ext, create(Command_CreateGameSchema, gameConfig), { onSuccess: () => { RoomPersistence.gameCreated(roomId); }, diff --git a/webclient/src/websocket/commands/room/joinGame.ts b/webclient/src/websocket/commands/room/joinGame.ts index ef4b1fff2..3c5db7d98 100644 --- a/webclient/src/websocket/commands/room/joinGame.ts +++ b/webclient/src/websocket/commands/room/joinGame.ts @@ -1,9 +1,11 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_JoinGame_ext, Command_JoinGameSchema } from 'generated/proto/room_commands_pb'; import { RoomPersistence } from '../../persistence'; import { JoinGameParams } from 'types'; export function joinGame(roomId: number, joinGameParams: JoinGameParams): void { - BackendService.sendRoomCommand(roomId, 'Command_JoinGame', joinGameParams, { + BackendService.sendRoomCommand(roomId, Command_JoinGame_ext, create(Command_JoinGameSchema, joinGameParams), { onSuccess: () => { RoomPersistence.joinedGame(roomId, joinGameParams.gameId); }, diff --git a/webclient/src/websocket/commands/room/leaveRoom.ts b/webclient/src/websocket/commands/room/leaveRoom.ts index 7cd64a0e2..445f15ee3 100644 --- a/webclient/src/websocket/commands/room/leaveRoom.ts +++ b/webclient/src/websocket/commands/room/leaveRoom.ts @@ -1,8 +1,10 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_LeaveRoom_ext, Command_LeaveRoomSchema } from 'generated/proto/room_commands_pb'; import { RoomPersistence } from '../../persistence'; export function leaveRoom(roomId: number): void { - BackendService.sendRoomCommand(roomId, 'Command_LeaveRoom', {}, { + BackendService.sendRoomCommand(roomId, Command_LeaveRoom_ext, create(Command_LeaveRoomSchema), { onSuccess: () => { RoomPersistence.leaveRoom(roomId); }, diff --git a/webclient/src/websocket/commands/room/roomCommands.spec.ts b/webclient/src/websocket/commands/room/roomCommands.spec.ts index d700ced15..cf5693407 100644 --- a/webclient/src/websocket/commands/room/roomCommands.spec.ts +++ b/webclient/src/websocket/commands/room/roomCommands.spec.ts @@ -15,6 +15,7 @@ vi.mock('../../persistence', () => ({ import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; import { BackendService } from '../../services/BackendService'; import { RoomPersistence } from '../../persistence'; +import { Command_CreateGame_ext, Command_JoinGame_ext, Command_LeaveRoom_ext, Command_RoomSay_ext } from 'generated/proto/room_commands_pb'; import { createGame } from './createGame'; import { joinGame } from './joinGame'; import { leaveRoom } from './leaveRoom'; @@ -22,7 +23,7 @@ import { roomSay } from './roomSay'; const { getLastSendOpts, invokeOnSuccess } = makeCallbackHelpers( BackendService.sendRoomCommand as vi.Mock, - // sendRoomCommand(roomId, commandName, params, options) — options at index 3 + // sendRoomCommand(roomId, ext, value, options) — options at index 3 3 ); @@ -35,7 +36,9 @@ describe('createGame', () => { it('calls sendRoomCommand with Command_CreateGame', () => { createGame(5, { maxPlayers: 4 } as any); - expect(BackendService.sendRoomCommand).toHaveBeenCalledWith(5, 'Command_CreateGame', { maxPlayers: 4 }, expect.any(Object)); + expect(BackendService.sendRoomCommand).toHaveBeenCalledWith( + 5, Command_CreateGame_ext, expect.objectContaining({ maxPlayers: 4 }), expect.any(Object) + ); }); it('onSuccess calls RoomPersistence.gameCreated with roomId', () => { @@ -52,7 +55,9 @@ describe('joinGame', () => { it('calls sendRoomCommand with Command_JoinGame', () => { joinGame(7, { gameId: 42, password: '' } as any); - expect(BackendService.sendRoomCommand).toHaveBeenCalledWith(7, 'Command_JoinGame', { gameId: 42, password: '' }, expect.any(Object)); + expect(BackendService.sendRoomCommand).toHaveBeenCalledWith( + 7, Command_JoinGame_ext, expect.objectContaining({ gameId: 42, password: '' }), expect.any(Object) + ); }); it('onSuccess calls RoomPersistence.joinedGame with roomId and gameId', () => { @@ -69,7 +74,7 @@ describe('leaveRoom', () => { it('calls sendRoomCommand with Command_LeaveRoom', () => { leaveRoom(3); - expect(BackendService.sendRoomCommand).toHaveBeenCalledWith(3, 'Command_LeaveRoom', {}, expect.any(Object)); + expect(BackendService.sendRoomCommand).toHaveBeenCalledWith(3, Command_LeaveRoom_ext, expect.any(Object), expect.any(Object)); }); it('onSuccess calls RoomPersistence.leaveRoom with roomId', () => { @@ -86,7 +91,11 @@ describe('roomSay', () => { it('calls sendRoomCommand with trimmed message', () => { roomSay(2, ' hello '); - expect(BackendService.sendRoomCommand).toHaveBeenCalledWith(2, 'Command_RoomSay', { message: 'hello' }, expect.any(Object)); + expect(BackendService.sendRoomCommand).toHaveBeenCalledWith( + 2, + Command_RoomSay_ext, + expect.objectContaining({ message: 'hello' }) + ); }); it('does not call sendRoomCommand when message is blank', () => { diff --git a/webclient/src/websocket/commands/room/roomSay.ts b/webclient/src/websocket/commands/room/roomSay.ts index a429845be..0ebf62b00 100644 --- a/webclient/src/websocket/commands/room/roomSay.ts +++ b/webclient/src/websocket/commands/room/roomSay.ts @@ -1,4 +1,6 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_RoomSay_ext, Command_RoomSaySchema } from 'generated/proto/room_commands_pb'; export function roomSay(roomId: number, message: string): void { const trimmed = message.trim(); @@ -7,5 +9,5 @@ export function roomSay(roomId: number, message: string): void { return; } - BackendService.sendRoomCommand(roomId, 'Command_RoomSay', { message: trimmed }, {}); + BackendService.sendRoomCommand(roomId, Command_RoomSay_ext, create(Command_RoomSaySchema, { message: trimmed })); } diff --git a/webclient/src/websocket/commands/session/accountEdit.ts b/webclient/src/websocket/commands/session/accountEdit.ts index 31bf2d3f6..f36f48bf9 100644 --- a/webclient/src/websocket/commands/session/accountEdit.ts +++ b/webclient/src/websocket/commands/session/accountEdit.ts @@ -1,8 +1,11 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_AccountEdit_ext, Command_AccountEditSchema } from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; export function accountEdit(passwordCheck: string, realName?: string, email?: string, country?: string): void { - BackendService.sendSessionCommand('Command_AccountEdit', { passwordCheck, realName, email, country }, { + const cmd = create(Command_AccountEditSchema, { passwordCheck, realName, email, country }); + BackendService.sendSessionCommand(Command_AccountEdit_ext, cmd, { onSuccess: () => { SessionPersistence.accountEditChanged(realName, email, country); }, diff --git a/webclient/src/websocket/commands/session/accountImage.ts b/webclient/src/websocket/commands/session/accountImage.ts index cd0e24403..8a5eebe18 100644 --- a/webclient/src/websocket/commands/session/accountImage.ts +++ b/webclient/src/websocket/commands/session/accountImage.ts @@ -1,8 +1,10 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_AccountImage_ext, Command_AccountImageSchema } from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; export function accountImage(image: Uint8Array): void { - BackendService.sendSessionCommand('Command_AccountImage', { image }, { + BackendService.sendSessionCommand(Command_AccountImage_ext, create(Command_AccountImageSchema, { image }), { onSuccess: () => { SessionPersistence.accountImageChanged(image); }, diff --git a/webclient/src/websocket/commands/session/accountPassword.ts b/webclient/src/websocket/commands/session/accountPassword.ts index 81c7a993b..7a06ca6d7 100644 --- a/webclient/src/websocket/commands/session/accountPassword.ts +++ b/webclient/src/websocket/commands/session/accountPassword.ts @@ -1,8 +1,11 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_AccountPassword_ext, Command_AccountPasswordSchema } from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; export function accountPassword(oldPassword: string, newPassword: string, hashedNewPassword: string): void { - BackendService.sendSessionCommand('Command_AccountPassword', { oldPassword, newPassword, hashedNewPassword }, { + const cmd = create(Command_AccountPasswordSchema, { oldPassword, newPassword, hashedNewPassword }); + BackendService.sendSessionCommand(Command_AccountPassword_ext, cmd, { onSuccess: () => { SessionPersistence.accountPasswordChange(); }, diff --git a/webclient/src/websocket/commands/session/activate.ts b/webclient/src/websocket/commands/session/activate.ts index 6fd909ca8..25340d106 100644 --- a/webclient/src/websocket/commands/session/activate.ts +++ b/webclient/src/websocket/commands/session/activate.ts @@ -1,23 +1,25 @@ import { AccountActivationParams } from 'store'; import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; import { BackendService } from '../../services/BackendService'; -import { ProtoController } from '../../services/ProtoController'; +import { Command_Activate_ext, Command_ActivateSchema } from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; +import { Response_ResponseCode } from 'generated/proto/response_pb'; import { disconnect, login, updateStatus } from './'; export function activate(options: WebSocketConnectOptions, password?: string, passwordSalt?: string): void { const { userName, token } = options as unknown as AccountActivationParams; - BackendService.sendSessionCommand('Command_Activate', { + BackendService.sendSessionCommand(Command_Activate_ext, create(Command_ActivateSchema, { ...webClient.clientConfig, userName, token, - }, { + }), { onResponseCode: { - [ProtoController.root.Response.ResponseCode.RespActivationAccepted]: () => { + [Response_ResponseCode.RespActivationAccepted]: () => { SessionPersistence.accountActivationSuccess(); login(options, password, passwordSalt); }, diff --git a/webclient/src/websocket/commands/session/addToList.ts b/webclient/src/websocket/commands/session/addToList.ts index c5bc3c5f0..7dbee06e2 100644 --- a/webclient/src/websocket/commands/session/addToList.ts +++ b/webclient/src/websocket/commands/session/addToList.ts @@ -1,4 +1,6 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_AddToList_ext, Command_AddToListSchema } from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; export function addToBuddyList(userName: string): void { @@ -10,7 +12,7 @@ export function addToIgnoreList(userName: string): void { } export function addToList(list: string, userName: string): void { - BackendService.sendSessionCommand('Command_AddToList', { list, userName }, { + BackendService.sendSessionCommand(Command_AddToList_ext, create(Command_AddToListSchema, { list, userName }), { onSuccess: () => { SessionPersistence.addToList(list, userName); }, diff --git a/webclient/src/websocket/commands/session/deckDel.ts b/webclient/src/websocket/commands/session/deckDel.ts index 752ce78d5..04816ad46 100644 --- a/webclient/src/websocket/commands/session/deckDel.ts +++ b/webclient/src/websocket/commands/session/deckDel.ts @@ -1,8 +1,10 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_DeckDelSchema, Command_DeckDel_ext } from 'generated/proto/command_deck_del_pb'; import { SessionPersistence } from '../../persistence'; export function deckDel(deckId: number): void { - BackendService.sendSessionCommand('Command_DeckDel', { deckId }, { + BackendService.sendSessionCommand(Command_DeckDel_ext, create(Command_DeckDelSchema, { deckId }), { onSuccess: () => { SessionPersistence.deleteServerDeck(deckId); }, diff --git a/webclient/src/websocket/commands/session/deckDelDir.ts b/webclient/src/websocket/commands/session/deckDelDir.ts index df5bbc223..1d7782e2d 100644 --- a/webclient/src/websocket/commands/session/deckDelDir.ts +++ b/webclient/src/websocket/commands/session/deckDelDir.ts @@ -1,8 +1,10 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_DeckDelDirSchema, Command_DeckDelDir_ext } from 'generated/proto/command_deck_del_dir_pb'; import { SessionPersistence } from '../../persistence'; export function deckDelDir(path: string): void { - BackendService.sendSessionCommand('Command_DeckDelDir', { path }, { + BackendService.sendSessionCommand(Command_DeckDelDir_ext, create(Command_DeckDelDirSchema, { path }), { onSuccess: () => { SessionPersistence.deleteServerDeckDir(path); }, diff --git a/webclient/src/websocket/commands/session/deckList.ts b/webclient/src/websocket/commands/session/deckList.ts index 3d5a3499a..dc526d5be 100644 --- a/webclient/src/websocket/commands/session/deckList.ts +++ b/webclient/src/websocket/commands/session/deckList.ts @@ -1,9 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_DeckListSchema, Command_DeckList_ext } from 'generated/proto/command_deck_list_pb'; import { SessionPersistence } from '../../persistence'; +import { Response_DeckList_ext } from 'generated/proto/response_deck_list_pb'; export function deckList(): void { - BackendService.sendSessionCommand('Command_DeckList', {}, { - responseName: 'Response_DeckList', + BackendService.sendSessionCommand(Command_DeckList_ext, create(Command_DeckListSchema), { + responseExt: Response_DeckList_ext, onSuccess: (response) => { SessionPersistence.updateServerDecks(response); }, diff --git a/webclient/src/websocket/commands/session/deckNewDir.ts b/webclient/src/websocket/commands/session/deckNewDir.ts index 85ab16afb..dc6f75481 100644 --- a/webclient/src/websocket/commands/session/deckNewDir.ts +++ b/webclient/src/websocket/commands/session/deckNewDir.ts @@ -1,8 +1,10 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_DeckNewDirSchema, Command_DeckNewDir_ext } from 'generated/proto/command_deck_new_dir_pb'; import { SessionPersistence } from '../../persistence'; export function deckNewDir(path: string, dirName: string): void { - BackendService.sendSessionCommand('Command_DeckNewDir', { path, dirName }, { + BackendService.sendSessionCommand(Command_DeckNewDir_ext, create(Command_DeckNewDirSchema, { path, dirName }), { onSuccess: () => { SessionPersistence.createServerDeckDir(path, dirName); }, diff --git a/webclient/src/websocket/commands/session/deckUpload.ts b/webclient/src/websocket/commands/session/deckUpload.ts index 2679c4e8e..fce574525 100644 --- a/webclient/src/websocket/commands/session/deckUpload.ts +++ b/webclient/src/websocket/commands/session/deckUpload.ts @@ -1,9 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_DeckUploadSchema, Command_DeckUpload_ext } from 'generated/proto/command_deck_upload_pb'; import { SessionPersistence } from '../../persistence'; +import { Response_DeckUpload_ext } from 'generated/proto/response_deck_upload_pb'; export function deckUpload(path: string, deckId: number, deckList: string): void { - BackendService.sendSessionCommand('Command_DeckUpload', { path, deckId, deckList }, { - responseName: 'Response_DeckUpload', + BackendService.sendSessionCommand(Command_DeckUpload_ext, create(Command_DeckUploadSchema, { path, deckId, deckList }), { + responseExt: Response_DeckUpload_ext, onSuccess: (response) => { SessionPersistence.uploadServerDeck(path, response.newFile); }, diff --git a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts index 05af1ccf9..71d105f8d 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts @@ -1,19 +1,23 @@ import { ForgotPasswordChallengeParams } from 'store'; import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; import { BackendService } from '../../services/BackendService'; +import { + Command_ForgotPasswordChallenge_ext, Command_ForgotPasswordChallengeSchema, +} from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; import { disconnect, updateStatus } from './'; export function forgotPasswordChallenge(options: WebSocketConnectOptions): void { const { userName, email } = options as unknown as ForgotPasswordChallengeParams; - BackendService.sendSessionCommand('Command_ForgotPasswordChallenge', { + BackendService.sendSessionCommand(Command_ForgotPasswordChallenge_ext, create(Command_ForgotPasswordChallengeSchema, { ...webClient.clientConfig, userName, email, - }, { + }), { onSuccess: () => { updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPassword(); diff --git a/webclient/src/websocket/commands/session/forgotPasswordRequest.ts b/webclient/src/websocket/commands/session/forgotPasswordRequest.ts index 23d301450..a14cd1ee5 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordRequest.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordRequest.ts @@ -1,20 +1,25 @@ import { ForgotPasswordParams } from 'store'; import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; import { BackendService } from '../../services/BackendService'; +import { + Command_ForgotPasswordRequest_ext, Command_ForgotPasswordRequestSchema, +} from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; +import { Response_ForgotPasswordRequest_ext } from 'generated/proto/response_forgotpasswordrequest_pb'; import { disconnect, updateStatus } from './'; export function forgotPasswordRequest(options: WebSocketConnectOptions): void { const { userName } = options as unknown as ForgotPasswordParams; - BackendService.sendSessionCommand('Command_ForgotPasswordRequest', { + BackendService.sendSessionCommand(Command_ForgotPasswordRequest_ext, create(Command_ForgotPasswordRequestSchema, { ...webClient.clientConfig, userName, - }, { - responseName: 'Response_ForgotPasswordRequest', + }), { + responseExt: Response_ForgotPasswordRequest_ext, onSuccess: (resp) => { if (resp?.challengeEmail) { updateStatus(StatusEnum.DISCONNECTED, null); diff --git a/webclient/src/websocket/commands/session/forgotPasswordReset.ts b/webclient/src/websocket/commands/session/forgotPasswordReset.ts index 21e5842f6..543467b18 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordReset.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordReset.ts @@ -1,8 +1,13 @@ import { ForgotPasswordResetParams } from 'store'; import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { create } from '@bufbuild/protobuf'; +import type { MessageInitShape } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; import { BackendService } from '../../services/BackendService'; +import { + Command_ForgotPasswordReset_ext, Command_ForgotPasswordResetSchema, +} from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; import { hashPassword } from '../../utils'; @@ -11,19 +16,16 @@ import { disconnect, updateStatus } from '.'; export function forgotPasswordReset(options: WebSocketConnectOptions, newPassword?: string, passwordSalt?: string): void { const { userName, token } = options as unknown as ForgotPasswordResetParams; - const params: any = { + const params: MessageInitShape = { ...webClient.clientConfig, userName, token, + ...(passwordSalt + ? { hashedNewPassword: hashPassword(passwordSalt, newPassword) } + : { newPassword }), }; - if (passwordSalt) { - params.hashedNewPassword = hashPassword(passwordSalt, newPassword); - } else { - params.newPassword = newPassword; - } - - BackendService.sendSessionCommand('Command_ForgotPasswordReset', params, { + BackendService.sendSessionCommand(Command_ForgotPasswordReset_ext, create(Command_ForgotPasswordResetSchema, params), { onSuccess: () => { updateStatus(StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordSuccess(); diff --git a/webclient/src/websocket/commands/session/getGamesOfUser.ts b/webclient/src/websocket/commands/session/getGamesOfUser.ts index 8fb8aeb5b..4f2cb2109 100644 --- a/webclient/src/websocket/commands/session/getGamesOfUser.ts +++ b/webclient/src/websocket/commands/session/getGamesOfUser.ts @@ -1,9 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_GetGamesOfUser_ext, Command_GetGamesOfUserSchema } from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; +import { Response_GetGamesOfUser_ext } from 'generated/proto/response_get_games_of_user_pb'; export function getGamesOfUser(userName: string): void { - BackendService.sendSessionCommand('Command_GetGamesOfUser', { userName }, { - responseName: 'Response_GetGamesOfUser', + BackendService.sendSessionCommand(Command_GetGamesOfUser_ext, create(Command_GetGamesOfUserSchema, { userName }), { + responseExt: Response_GetGamesOfUser_ext, onSuccess: (response) => { SessionPersistence.getGamesOfUser(userName, response); }, diff --git a/webclient/src/websocket/commands/session/getUserInfo.ts b/webclient/src/websocket/commands/session/getUserInfo.ts index 5b0f178ae..2a93e81d1 100644 --- a/webclient/src/websocket/commands/session/getUserInfo.ts +++ b/webclient/src/websocket/commands/session/getUserInfo.ts @@ -1,9 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_GetUserInfo_ext, Command_GetUserInfoSchema } from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; +import { Response_GetUserInfo_ext } from 'generated/proto/response_get_user_info_pb'; export function getUserInfo(userName: string): void { - BackendService.sendSessionCommand('Command_GetUserInfo', { userName }, { - responseName: 'Response_GetUserInfo', + BackendService.sendSessionCommand(Command_GetUserInfo_ext, create(Command_GetUserInfoSchema, { userName }), { + responseExt: Response_GetUserInfo_ext, onSuccess: (response) => { SessionPersistence.getUserInfo(response.userInfo); }, diff --git a/webclient/src/websocket/commands/session/joinRoom.ts b/webclient/src/websocket/commands/session/joinRoom.ts index be79976a0..657acd3c7 100644 --- a/webclient/src/websocket/commands/session/joinRoom.ts +++ b/webclient/src/websocket/commands/session/joinRoom.ts @@ -1,9 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_JoinRoom_ext, Command_JoinRoomSchema } from 'generated/proto/session_commands_pb'; import { RoomPersistence } from '../../persistence'; +import { Response_JoinRoom_ext } from 'generated/proto/response_join_room_pb'; export function joinRoom(roomId: number): void { - BackendService.sendSessionCommand('Command_JoinRoom', { roomId }, { - responseName: 'Response_JoinRoom', + BackendService.sendSessionCommand(Command_JoinRoom_ext, create(Command_JoinRoomSchema, { roomId }), { + responseExt: Response_JoinRoom_ext, onSuccess: (response) => { RoomPersistence.joinRoom(response.roomInfo); }, diff --git a/webclient/src/websocket/commands/session/listRooms.ts b/webclient/src/websocket/commands/session/listRooms.ts index 367dada9b..9a4efde14 100644 --- a/webclient/src/websocket/commands/session/listRooms.ts +++ b/webclient/src/websocket/commands/session/listRooms.ts @@ -1,5 +1,7 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ListRooms_ext, Command_ListRoomsSchema } from 'generated/proto/session_commands_pb'; export function listRooms(): void { - BackendService.sendSessionCommand('Command_ListRooms', {}, {}); + BackendService.sendSessionCommand(Command_ListRooms_ext, create(Command_ListRoomsSchema)); } diff --git a/webclient/src/websocket/commands/session/listUsers.ts b/webclient/src/websocket/commands/session/listUsers.ts index 9b95c1344..e10fe2ba5 100644 --- a/webclient/src/websocket/commands/session/listUsers.ts +++ b/webclient/src/websocket/commands/session/listUsers.ts @@ -1,9 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ListUsers_ext, Command_ListUsersSchema } from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; +import { Response_ListUsers_ext } from 'generated/proto/response_list_users_pb'; export function listUsers(): void { - BackendService.sendSessionCommand('Command_ListUsers', {}, { - responseName: 'Response_ListUsers', + BackendService.sendSessionCommand(Command_ListUsers_ext, create(Command_ListUsersSchema), { + responseExt: Response_ListUsers_ext, onSuccess: (response) => { SessionPersistence.updateUsers(response.userList); }, diff --git a/webclient/src/websocket/commands/session/login.ts b/webclient/src/websocket/commands/session/login.ts index adbd45b5d..68b0cd896 100644 --- a/webclient/src/websocket/commands/session/login.ts +++ b/webclient/src/websocket/commands/session/login.ts @@ -1,9 +1,13 @@ import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { create } from '@bufbuild/protobuf'; +import type { MessageInitShape } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; import { BackendService } from '../../services/BackendService'; -import { ProtoController } from '../../services/ProtoController'; +import { Command_Login_ext, Command_LoginSchema } from 'generated/proto/session_commands_pb'; import { hashPassword } from '../../utils'; import { SessionPersistence } from '../../persistence'; +import { Response_Login_ext } from 'generated/proto/response_login_pb'; +import { Response_ResponseCode } from 'generated/proto/response_pb'; import { disconnect, @@ -15,20 +19,15 @@ import { export function login(options: WebSocketConnectOptions, password?: string, passwordSalt?: string): void { const { userName, hashedPassword } = options; - const loginConfig: any = { + const loginConfig: MessageInitShape = { ...webClient.clientConfig, clientid: 'webatrice', userName, + ...(passwordSalt + ? { hashedPassword: hashedPassword || hashPassword(passwordSalt, password) } + : { password }), }; - if (passwordSalt) { - loginConfig.hashedPassword = hashedPassword || hashPassword(passwordSalt, password); - } else { - loginConfig.password = password; - } - - const { ResponseCode } = ProtoController.root.Response; - const onLoginError = (message: string, extra?: () => void) => { updateStatus(StatusEnum.DISCONNECTED, message); extra?.(); @@ -36,8 +35,8 @@ export function login(options: WebSocketConnectOptions, password?: string, passw disconnect(); }; - BackendService.sendSessionCommand('Command_Login', loginConfig, { - responseName: 'Response_Login', + BackendService.sendSessionCommand(Command_Login_ext, create(Command_LoginSchema, loginConfig), { + responseExt: Response_Login_ext, onSuccess: (resp) => { const { buddyList, ignoreList, userInfo } = resp; @@ -53,23 +52,23 @@ export function login(options: WebSocketConnectOptions, password?: string, passw updateStatus(StatusEnum.LOGGED_IN, 'Logged in.'); }, onResponseCode: { - [ResponseCode.RespClientUpdateRequired]: () => + [Response_ResponseCode.RespClientUpdateRequired]: () => onLoginError('Login failed: missing features'), - [ResponseCode.RespWrongPassword]: () => + [Response_ResponseCode.RespWrongPassword]: () => onLoginError('Login failed: incorrect username or password'), - [ResponseCode.RespUsernameInvalid]: () => + [Response_ResponseCode.RespUsernameInvalid]: () => onLoginError('Login failed: incorrect username or password'), - [ResponseCode.RespWouldOverwriteOldSession]: () => + [Response_ResponseCode.RespWouldOverwriteOldSession]: () => onLoginError('Login failed: duplicated user session'), - [ResponseCode.RespUserIsBanned]: () => + [Response_ResponseCode.RespUserIsBanned]: () => onLoginError('Login failed: banned user'), - [ResponseCode.RespRegistrationRequired]: () => + [Response_ResponseCode.RespRegistrationRequired]: () => onLoginError('Login failed: registration required'), - [ResponseCode.RespClientIdRequired]: () => + [Response_ResponseCode.RespClientIdRequired]: () => onLoginError('Login failed: missing client ID'), - [ResponseCode.RespContextError]: () => + [Response_ResponseCode.RespContextError]: () => onLoginError('Login failed: server error'), - [ResponseCode.RespAccountNotActivated]: () => + [Response_ResponseCode.RespAccountNotActivated]: () => onLoginError('Login failed: account not activated', () => { const { password: _p, newPassword: _np, ...safeOptions } = options; diff --git a/webclient/src/websocket/commands/session/message.ts b/webclient/src/websocket/commands/session/message.ts index b6bde9cac..19946779e 100644 --- a/webclient/src/websocket/commands/session/message.ts +++ b/webclient/src/websocket/commands/session/message.ts @@ -1,5 +1,7 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_Message_ext, Command_MessageSchema } from 'generated/proto/session_commands_pb'; export function message(userName: string, message: string): void { - BackendService.sendSessionCommand('Command_Message', { userName, message }, {}); + BackendService.sendSessionCommand(Command_Message_ext, create(Command_MessageSchema, { userName, message })); } diff --git a/webclient/src/websocket/commands/session/ping.ts b/webclient/src/websocket/commands/session/ping.ts index fea2784a2..11a7ff4fc 100644 --- a/webclient/src/websocket/commands/session/ping.ts +++ b/webclient/src/websocket/commands/session/ping.ts @@ -1,7 +1,9 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_Ping_ext, Command_PingSchema } from 'generated/proto/session_commands_pb'; export function ping(pingReceived: Function): void { - BackendService.sendSessionCommand('Command_Ping', {}, { + BackendService.sendSessionCommand(Command_Ping_ext, create(Command_PingSchema), { onResponse: (raw) => pingReceived(raw), }); } diff --git a/webclient/src/websocket/commands/session/register.ts b/webclient/src/websocket/commands/session/register.ts index 31bd37a09..22d80d923 100644 --- a/webclient/src/websocket/commands/session/register.ts +++ b/webclient/src/websocket/commands/session/register.ts @@ -1,73 +1,71 @@ import { ServerRegisterParams } from 'store'; import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { create } from '@bufbuild/protobuf'; +import type { MessageInitShape } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; import { BackendService } from '../../services/BackendService'; -import { ProtoController } from '../../services/ProtoController'; +import { Command_Register_ext, Command_RegisterSchema } from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; import { hashPassword } from '../../utils'; +import { Response_ResponseCode } from 'generated/proto/response_pb'; import { login, disconnect, updateStatus } from './'; export function register(options: WebSocketConnectOptions, password?: string, passwordSalt?: string): void { const { userName, email, country, realName } = options as ServerRegisterParams; - const params: any = { + const params: MessageInitShape = { ...webClient.clientConfig, userName, email, country, realName, + ...(passwordSalt + ? { hashedPassword: hashPassword(passwordSalt, password) } + : { password }), }; - if (passwordSalt) { - params.hashedPassword = hashPassword(passwordSalt, password); - } else { - params.password = password; - } - - const { ResponseCode } = ProtoController.root.Response; - const onRegistrationError = (action: () => void) => { action(); updateStatus(StatusEnum.DISCONNECTED, 'Registration failed'); disconnect(); }; - BackendService.sendSessionCommand('Command_Register', params, { + BackendService.sendSessionCommand(Command_Register_ext, create(Command_RegisterSchema, params), { onResponseCode: { - [ResponseCode.RespRegistrationAccepted]: () => { + [Response_ResponseCode.RespRegistrationAccepted]: () => { login(options, password, passwordSalt); SessionPersistence.registrationSuccess(); }, - [ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => { + [Response_ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => { updateStatus(StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); const { password: _p, newPassword: _np, ...safeOptions } = options; SessionPersistence.accountAwaitingActivation(safeOptions); disconnect(); }, - [ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( + [Response_ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( () => SessionPersistence.registrationUserNameError('Username is taken') ), - [ResponseCode.RespUsernameInvalid]: () => onRegistrationError( + [Response_ResponseCode.RespUsernameInvalid]: () => onRegistrationError( () => SessionPersistence.registrationUserNameError('Invalid username') ), - [ResponseCode.RespPasswordTooShort]: () => onRegistrationError( + [Response_ResponseCode.RespPasswordTooShort]: () => onRegistrationError( () => SessionPersistence.registrationPasswordError('Your password was too short') ), - [ResponseCode.RespEmailRequiredToRegister]: () => onRegistrationError( + [Response_ResponseCode.RespEmailRequiredToRegister]: () => onRegistrationError( () => SessionPersistence.registrationRequiresEmail() ), - [ResponseCode.RespEmailBlackListed]: () => onRegistrationError( + [Response_ResponseCode.RespEmailBlackListed]: () => onRegistrationError( () => SessionPersistence.registrationEmailError('This email provider has been blocked') ), - [ResponseCode.RespTooManyRequests]: () => onRegistrationError( + [Response_ResponseCode.RespTooManyRequests]: () => onRegistrationError( () => SessionPersistence.registrationEmailError('Max accounts reached for this email') ), - [ResponseCode.RespRegistrationDisabled]: () => onRegistrationError( + [Response_ResponseCode.RespRegistrationDisabled]: () => onRegistrationError( () => SessionPersistence.registrationFailed('Registration is currently disabled') ), - [ResponseCode.RespUserIsBanned]: (raw) => onRegistrationError( + [Response_ResponseCode.RespUserIsBanned]: (raw) => onRegistrationError( () => SessionPersistence.registrationFailed(raw.reasonStr, raw.endTime) ), }, diff --git a/webclient/src/websocket/commands/session/removeFromList.ts b/webclient/src/websocket/commands/session/removeFromList.ts index aede49c49..ff1254e27 100644 --- a/webclient/src/websocket/commands/session/removeFromList.ts +++ b/webclient/src/websocket/commands/session/removeFromList.ts @@ -1,4 +1,6 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_RemoveFromList_ext, Command_RemoveFromListSchema } from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; export function removeFromBuddyList(userName: string): void { @@ -10,7 +12,7 @@ export function removeFromIgnoreList(userName: string): void { } export function removeFromList(list: string, userName: string): void { - BackendService.sendSessionCommand('Command_RemoveFromList', { list, userName }, { + BackendService.sendSessionCommand(Command_RemoveFromList_ext, create(Command_RemoveFromListSchema, { list, userName }), { onSuccess: () => { SessionPersistence.removeFromList(list, userName); }, diff --git a/webclient/src/websocket/commands/session/replayDeleteMatch.ts b/webclient/src/websocket/commands/session/replayDeleteMatch.ts index 24ac48f1c..7d243e1b2 100644 --- a/webclient/src/websocket/commands/session/replayDeleteMatch.ts +++ b/webclient/src/websocket/commands/session/replayDeleteMatch.ts @@ -1,8 +1,10 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ReplayDeleteMatchSchema, Command_ReplayDeleteMatch_ext } from 'generated/proto/command_replay_delete_match_pb'; import { SessionPersistence } from '../../persistence'; export function replayDeleteMatch(gameId: number): void { - BackendService.sendSessionCommand('Command_ReplayDeleteMatch', { gameId }, { + BackendService.sendSessionCommand(Command_ReplayDeleteMatch_ext, create(Command_ReplayDeleteMatchSchema, { gameId }), { onSuccess: () => { SessionPersistence.replayDeleteMatch(gameId); }, diff --git a/webclient/src/websocket/commands/session/replayGetCode.ts b/webclient/src/websocket/commands/session/replayGetCode.ts index 1e0557d1c..77f72c330 100644 --- a/webclient/src/websocket/commands/session/replayGetCode.ts +++ b/webclient/src/websocket/commands/session/replayGetCode.ts @@ -1,8 +1,11 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ReplayGetCodeSchema, Command_ReplayGetCode_ext } from 'generated/proto/command_replay_get_code_pb'; +import { Response_ReplayGetCode_ext } from 'generated/proto/response_replay_get_code_pb'; export function replayGetCode(gameId: number, onCodeReceived: (code: string) => void): void { - BackendService.sendSessionCommand('Command_ReplayGetCode', { gameId }, { - responseName: 'Response_ReplayGetCode', + BackendService.sendSessionCommand(Command_ReplayGetCode_ext, create(Command_ReplayGetCodeSchema, { gameId }), { + responseExt: Response_ReplayGetCode_ext, onSuccess: (response) => { onCodeReceived(response.replayCode); }, diff --git a/webclient/src/websocket/commands/session/replayList.ts b/webclient/src/websocket/commands/session/replayList.ts index f39eb279f..1d6dc8aa5 100644 --- a/webclient/src/websocket/commands/session/replayList.ts +++ b/webclient/src/websocket/commands/session/replayList.ts @@ -1,9 +1,12 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ReplayListSchema, Command_ReplayList_ext } from 'generated/proto/command_replay_list_pb'; import { SessionPersistence } from '../../persistence'; +import { Response_ReplayList_ext } from 'generated/proto/response_replay_list_pb'; export function replayList(): void { - BackendService.sendSessionCommand('Command_ReplayList', {}, { - responseName: 'Response_ReplayList', + BackendService.sendSessionCommand(Command_ReplayList_ext, create(Command_ReplayListSchema), { + responseExt: Response_ReplayList_ext, onSuccess: (response) => { SessionPersistence.replayList(response.matchList); }, diff --git a/webclient/src/websocket/commands/session/replayModifyMatch.ts b/webclient/src/websocket/commands/session/replayModifyMatch.ts index 9825047f3..fa2af33c5 100644 --- a/webclient/src/websocket/commands/session/replayModifyMatch.ts +++ b/webclient/src/websocket/commands/session/replayModifyMatch.ts @@ -1,8 +1,10 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ReplayModifyMatchSchema, Command_ReplayModifyMatch_ext } from 'generated/proto/command_replay_modify_match_pb'; import { SessionPersistence } from '../../persistence'; export function replayModifyMatch(gameId: number, doNotHide: boolean): void { - BackendService.sendSessionCommand('Command_ReplayModifyMatch', { gameId, doNotHide }, { + BackendService.sendSessionCommand(Command_ReplayModifyMatch_ext, create(Command_ReplayModifyMatchSchema, { gameId, doNotHide }), { onSuccess: () => { SessionPersistence.replayModifyMatch(gameId, doNotHide); }, diff --git a/webclient/src/websocket/commands/session/replaySubmitCode.ts b/webclient/src/websocket/commands/session/replaySubmitCode.ts index ad1896e57..1ae371e8d 100644 --- a/webclient/src/websocket/commands/session/replaySubmitCode.ts +++ b/webclient/src/websocket/commands/session/replaySubmitCode.ts @@ -1,11 +1,13 @@ +import { create } from '@bufbuild/protobuf'; import { BackendService } from '../../services/BackendService'; +import { Command_ReplaySubmitCodeSchema, Command_ReplaySubmitCode_ext } from 'generated/proto/command_replay_submit_code_pb'; export function replaySubmitCode( replayCode: string, onSuccess?: () => void, onError?: (responseCode: number) => void, ): void { - BackendService.sendSessionCommand('Command_ReplaySubmitCode', { replayCode }, { + BackendService.sendSessionCommand(Command_ReplaySubmitCode_ext, create(Command_ReplaySubmitCodeSchema, { replayCode }), { onSuccess, onError, }); diff --git a/webclient/src/websocket/commands/session/requestPasswordSalt.ts b/webclient/src/websocket/commands/session/requestPasswordSalt.ts index e3635da70..5ba515719 100644 --- a/webclient/src/websocket/commands/session/requestPasswordSalt.ts +++ b/webclient/src/websocket/commands/session/requestPasswordSalt.ts @@ -1,10 +1,15 @@ import { RequestPasswordSaltParams } from 'store'; import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'types'; +import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; import { BackendService } from '../../services/BackendService'; -import { ProtoController } from '../../services/ProtoController'; +import { + Command_RequestPasswordSalt_ext, Command_RequestPasswordSaltSchema, +} from 'generated/proto/session_commands_pb'; import { SessionPersistence } from '../../persistence'; +import { Response_PasswordSalt_ext } from 'generated/proto/response_password_salt_pb'; +import { Response_ResponseCode } from 'generated/proto/response_pb'; import { activate, @@ -31,11 +36,11 @@ export function requestPasswordSalt(options: WebSocketConnectOptions, password?: disconnect(); }; - BackendService.sendSessionCommand('Command_RequestPasswordSalt', { + BackendService.sendSessionCommand(Command_RequestPasswordSalt_ext, create(Command_RequestPasswordSaltSchema, { ...webClient.clientConfig, userName, - }, { - responseName: 'Response_PasswordSalt', + }), { + responseExt: Response_PasswordSalt_ext, onSuccess: (resp) => { const passwordSalt = resp?.passwordSalt; @@ -51,7 +56,7 @@ export function requestPasswordSalt(options: WebSocketConnectOptions, password?: } }, onResponseCode: { - [ProtoController.root.Response.ResponseCode.RespRegistrationRequired]: () => { + [Response_ResponseCode.RespRegistrationRequired]: () => { updateStatus(StatusEnum.DISCONNECTED, 'Login failed: registration required'); onFailure(); }, diff --git a/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts b/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts index 8e8e2d02a..fe4227c4a 100644 --- a/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts +++ b/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts @@ -20,11 +20,6 @@ vi.mock('../../WebClient', async () => { return { __esModule: true, default: makeWebClientMock() }; }); -vi.mock('../../services/ProtoController', async () => { - const { makeProtoControllerRootMock } = await import('../../__mocks__/sessionCommandMocks'); - return { ProtoController: { root: makeProtoControllerRootMock() } }; -}); - vi.mock('../../utils', async () => { const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks'); return makeUtilsMock(); @@ -43,6 +38,19 @@ import webClient from '../../WebClient'; import * as SessionIndexMocks from './'; import { StatusEnum, WebSocketConnectReason } from 'types'; import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils'; +import { Response_ResponseCode } from 'generated/proto/response_pb'; +import { + Command_Activate_ext, + Command_ForgotPasswordChallenge_ext, + Command_ForgotPasswordRequest_ext, + Command_ForgotPasswordReset_ext, + Command_Login_ext, + Command_Register_ext, + Command_RequestPasswordSalt_ext, +} from 'generated/proto/session_commands_pb'; +import { Response_ForgotPasswordRequest_ext } from 'generated/proto/response_forgotpasswordrequest_pb'; +import { Response_Login_ext } from 'generated/proto/response_login_pb'; +import { Response_PasswordSalt_ext } from 'generated/proto/response_password_salt_pb'; import { connect } from './connect'; import { updateStatus } from './updateStatus'; import { login } from './login'; @@ -54,7 +62,8 @@ import { forgotPasswordReset } from './forgotPasswordReset'; import { requestPasswordSalt } from './requestPasswordSalt'; const { getLastSendOpts, invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpers( - BackendService.sendSessionCommand as vi.Mock + BackendService.sendSessionCommand as vi.Mock, + 2 ); beforeEach(() => { @@ -132,34 +141,34 @@ describe('login', () => { it('sends Command_Login with plain password when no salt', () => { login({ userName: 'alice' } as any, 'pw'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_Login', - expect.objectContaining({ userName: 'alice', password: 'pw' }), - expect.any(Object) + Command_Login_ext, + expect.objectContaining({ password: 'pw' }), + expect.objectContaining({ responseExt: Response_Login_ext }) ); }); it('sends Command_Login with hashedPassword when salt is given', () => { login({ userName: 'alice' } as any, 'pw', 'salt'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_Login', + Command_Login_ext, expect.objectContaining({ hashedPassword: 'hashed_pw' }), - expect.any(Object) + expect.objectContaining({ responseExt: Response_Login_ext }) ); }); it('uses options.hashedPassword if provided', () => { login({ userName: 'alice', hashedPassword: 'pre_hashed' } as any, 'pw', 'salt'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_Login', + Command_Login_ext, expect.objectContaining({ hashedPassword: 'pre_hashed' }), - expect.any(Object) + expect.objectContaining({ responseExt: Response_Login_ext }) ); }); it('onSuccess dispatches buddy/ignore/user and calls listUsers/listRooms', () => { login({ userName: 'alice' } as any, 'pw'); const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; - invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp }); + invokeOnSuccess(loginResp, { responseCode: 0 }); expect(SessionPersistence.updateBuddyList).toHaveBeenCalledWith([]); expect(SessionPersistence.updateIgnoreList).toHaveBeenCalledWith([]); expect(SessionPersistence.updateUser).toHaveBeenCalledWith({ name: 'alice' }); @@ -172,7 +181,7 @@ describe('login', () => { it('onSuccess does NOT pass plaintext password to loginSuccessful', () => { login({ userName: 'alice' } as any, 'secret'); const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; - invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp }); + invokeOnSuccess(loginResp, { responseCode: 0 }); const calledWith = (SessionPersistence.loginSuccessful as vi.Mock).mock.calls[0][0]; expect(calledWith).not.toHaveProperty('password'); }); @@ -180,63 +189,63 @@ describe('login', () => { it('onSuccess passes hashedPassword to loginSuccessful when salt is used', () => { login({ userName: 'alice' } as any, 'pw', 'salt'); const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; - invokeOnSuccess(loginResp, { responseCode: 0, '.Response_Login.ext': loginResp }); + invokeOnSuccess(loginResp, { responseCode: 0 }); const calledWith = (SessionPersistence.loginSuccessful as vi.Mock).mock.calls[0][0]; expect(calledWith).toHaveProperty('hashedPassword', 'hashed_pw'); }); it('onResponseCode RespClientUpdateRequired calls onLoginError', () => { login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(1); + invokeResponseCode(Response_ResponseCode.RespClientUpdateRequired); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('onResponseCode RespWrongPassword', () => { login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(2); + invokeResponseCode(Response_ResponseCode.RespWrongPassword); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespUsernameInvalid', () => { login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(3); + invokeResponseCode(Response_ResponseCode.RespUsernameInvalid); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespWouldOverwriteOldSession', () => { login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(4); + invokeResponseCode(Response_ResponseCode.RespWouldOverwriteOldSession); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespUserIsBanned', () => { login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(5); + invokeResponseCode(Response_ResponseCode.RespUserIsBanned); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespRegistrationRequired', () => { login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(6); + invokeResponseCode(Response_ResponseCode.RespRegistrationRequired); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespClientIdRequired', () => { login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(7); + invokeResponseCode(Response_ResponseCode.RespClientIdRequired); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespContextError', () => { login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(8); + invokeResponseCode(Response_ResponseCode.RespContextError); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespAccountNotActivated calls accountAwaitingActivation without password in options', () => { login({ userName: 'alice', password: 'leaked' } as any, 'pw'); - invokeResponseCode(9); + invokeResponseCode(Response_ResponseCode.RespAccountNotActivated); expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }) ); @@ -258,8 +267,8 @@ describe('register', () => { it('sends Command_Register with plain password when no salt', () => { register({ userName: 'alice', email: 'a@b.com', country: 'US', realName: 'Al' } as any, 'pw'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_Register', - expect.objectContaining({ userName: 'alice', password: 'pw' }), + Command_Register_ext, + expect.objectContaining({ password: 'pw' }), expect.any(Object) ); }); @@ -267,7 +276,7 @@ describe('register', () => { it('uses hashedPassword when salt is provided', () => { register({ userName: 'alice' } as any, 'pw', 'salt'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_Register', + Command_Register_ext, expect.objectContaining({ hashedPassword: 'hashed_pw' }), expect.any(Object) ); @@ -275,21 +284,21 @@ describe('register', () => { it('RespRegistrationAccepted calls login without salt and registrationSuccess', () => { register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(10); + invokeResponseCode(Response_ResponseCode.RespRegistrationAccepted); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', undefined); expect(SessionPersistence.registrationSuccess).toHaveBeenCalled(); }); it('RespRegistrationAccepted forwards salt to login', () => { register({ userName: 'alice' } as any, 'pw', 'mySalt'); - invokeResponseCode(10); + invokeResponseCode(Response_ResponseCode.RespRegistrationAccepted); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'mySalt'); expect(SessionPersistence.registrationSuccess).toHaveBeenCalled(); }); it('RespRegistrationAcceptedNeedsActivation calls accountAwaitingActivation without password in options', () => { register({ userName: 'alice', password: 'leaked' } as any, 'pw'); - invokeResponseCode(11); + invokeResponseCode(Response_ResponseCode.RespRegistrationAcceptedNeedsActivation); expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }) ); @@ -298,49 +307,49 @@ describe('register', () => { it('RespUserAlreadyExists calls registrationUserNameError', () => { register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(12); + invokeResponseCode(Response_ResponseCode.RespUserAlreadyExists); expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled(); }); it('RespUsernameInvalid calls registrationUserNameError', () => { register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(3); + invokeResponseCode(Response_ResponseCode.RespUsernameInvalid); expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled(); }); it('RespPasswordTooShort calls registrationPasswordError', () => { register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(13); + invokeResponseCode(Response_ResponseCode.RespPasswordTooShort); expect(SessionPersistence.registrationPasswordError).toHaveBeenCalled(); }); it('RespEmailRequiredToRegister calls registrationRequiresEmail', () => { register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(14); + invokeResponseCode(Response_ResponseCode.RespEmailRequiredToRegister); expect(SessionPersistence.registrationRequiresEmail).toHaveBeenCalled(); }); it('RespEmailBlackListed calls registrationEmailError', () => { register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(15); + invokeResponseCode(Response_ResponseCode.RespEmailBlackListed); expect(SessionPersistence.registrationEmailError).toHaveBeenCalled(); }); it('RespTooManyRequests calls registrationEmailError', () => { register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(16); + invokeResponseCode(Response_ResponseCode.RespTooManyRequests); expect(SessionPersistence.registrationEmailError).toHaveBeenCalled(); }); it('RespRegistrationDisabled calls registrationFailed', () => { register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(17); + invokeResponseCode(Response_ResponseCode.RespRegistrationDisabled); expect(SessionPersistence.registrationFailed).toHaveBeenCalled(); }); it('RespUserIsBanned calls registrationFailed with raw.reasonStr and raw.endTime', () => { register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(5, { reasonStr: 'bad user', endTime: 9999 }); + invokeResponseCode(Response_ResponseCode.RespUserIsBanned, { reasonStr: 'bad user', endTime: 9999 }); expect(SessionPersistence.registrationFailed).toHaveBeenCalledWith('bad user', 9999); }); @@ -359,12 +368,12 @@ describe('activate', () => { it('sends Command_Activate with userName and token, not password', () => { activate({ userName: 'alice', token: 'tok' } as any, 'pw'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_Activate', + Command_Activate_ext, expect.objectContaining({ userName: 'alice', token: 'tok' }), expect.any(Object) ); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_Activate', + Command_Activate_ext, expect.not.objectContaining({ password: expect.anything() }), expect.any(Object) ); @@ -372,7 +381,7 @@ describe('activate', () => { it('RespActivationAccepted calls accountActivationSuccess and forwards password+salt to login', () => { activate({ userName: 'alice', token: 'tok' } as any, 'pw', 'salt'); - invokeResponseCode(18); + invokeResponseCode(Response_ResponseCode.RespActivationAccepted); expect(SessionPersistence.accountActivationSuccess).toHaveBeenCalled(); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt'); }); @@ -393,7 +402,7 @@ describe('forgotPasswordChallenge', () => { it('sends Command_ForgotPasswordChallenge', () => { forgotPasswordChallenge({ userName: 'alice', email: 'a@b.com' } as any); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_ForgotPasswordChallenge', expect.any(Object), expect.any(Object) + Command_ForgotPasswordChallenge_ext, expect.any(Object), expect.any(Object) ); }); @@ -419,13 +428,17 @@ describe('forgotPasswordRequest', () => { it('sends Command_ForgotPasswordRequest', () => { forgotPasswordRequest({ userName: 'alice' } as any); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ForgotPasswordRequest', expect.any(Object), expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_ForgotPasswordRequest_ext, + expect.any(Object), + expect.objectContaining({ responseExt: Response_ForgotPasswordRequest_ext }) + ); }); it('onSuccess with challengeEmail calls resetPasswordChallenge', () => { forgotPasswordRequest({ userName: 'alice' } as any); const resp = { challengeEmail: true }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_ForgotPasswordRequest.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionPersistence.resetPasswordChallenge).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); @@ -433,7 +446,7 @@ describe('forgotPasswordRequest', () => { it('onSuccess without challengeEmail calls resetPassword', () => { forgotPasswordRequest({ userName: 'alice' } as any); const resp = { challengeEmail: false }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_ForgotPasswordRequest.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionPersistence.resetPassword).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); @@ -454,7 +467,7 @@ describe('forgotPasswordReset', () => { it('sends Command_ForgotPasswordReset with plain newPassword when no salt', () => { forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_ForgotPasswordReset', + Command_ForgotPasswordReset_ext, expect.objectContaining({ newPassword: 'newpw' }), expect.any(Object) ); @@ -463,7 +476,7 @@ describe('forgotPasswordReset', () => { it('sends hashed new password when salt provided', () => { forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw', 'salt'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_ForgotPasswordReset', + Command_ForgotPasswordReset_ext, expect.objectContaining({ hashedNewPassword: 'hashed_pw' }), expect.any(Object) ); @@ -491,40 +504,44 @@ describe('requestPasswordSalt', () => { it('sends Command_RequestPasswordSalt', () => { requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw'); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_RequestPasswordSalt', expect.any(Object), expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_RequestPasswordSalt_ext, + expect.any(Object), + expect.objectContaining({ responseExt: Response_PasswordSalt_ext }) + ); }); it('onSuccess with LOGIN reason forwards password+salt to login', () => { requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw'); const resp = { passwordSalt: 'salt123' }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt123'); }); it('onSuccess with ACTIVATE_ACCOUNT reason forwards password+salt to activate', () => { requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any, 'pw'); const resp = { passwordSalt: 'salt123' }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionIndexMocks.activate).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt123'); }); it('onSuccess with PASSWORD_RESET reason forwards newPassword+salt to forgotPasswordReset', () => { requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.PASSWORD_RESET } as any, undefined, 'newpw'); const resp = { passwordSalt: 'salt123' }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_PasswordSalt.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionIndexMocks.forgotPasswordReset).toHaveBeenCalledWith(expect.any(Object), 'newpw', 'salt123'); }); it('onResponseCode RespRegistrationRequired calls updateStatus and disconnect', () => { requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw'); - invokeResponseCode(6); + invokeResponseCode(Response_ResponseCode.RespRegistrationRequired); expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.any(String)); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('onResponseCode RespRegistrationRequired with ACTIVATE_ACCOUNT calls accountActivationFailed', () => { requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any, 'pw'); - invokeResponseCode(6); + invokeResponseCode(Response_ResponseCode.RespRegistrationRequired); expect(SessionPersistence.accountActivationFailed).toHaveBeenCalled(); }); diff --git a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts index c6daa0491..4cd8bdce4 100644 --- a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts +++ b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts @@ -19,11 +19,6 @@ vi.mock('../../WebClient', async () => { return { __esModule: true, default: makeWebClientMock() }; }); -vi.mock('../../services/ProtoController', async () => { - const { makeProtoControllerRootMock } = await import('../../__mocks__/sessionCommandMocks'); - return { ProtoController: { root: makeProtoControllerRootMock() } }; -}); - vi.mock('../../utils', async () => { const { makeUtilsMock } = await import('../../__mocks__/sessionCommandMocks'); return makeUtilsMock(); @@ -43,6 +38,38 @@ import { RoomPersistence } from '../../persistence'; import webClient from '../../WebClient'; import * as SessionCommands from './'; import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils'; +import { + Command_AccountEdit_ext, + Command_AccountImage_ext, + Command_AccountPassword_ext, + Command_AddToList_ext, + Command_GetGamesOfUser_ext, + Command_GetUserInfo_ext, + Command_JoinRoom_ext, + Command_ListRooms_ext, + Command_ListUsers_ext, + Command_Message_ext, + Command_Ping_ext, + Command_RemoveFromList_ext, +} from 'generated/proto/session_commands_pb'; +import { Command_DeckDel_ext } from 'generated/proto/command_deck_del_pb'; +import { Command_DeckDelDir_ext } from 'generated/proto/command_deck_del_dir_pb'; +import { Command_DeckList_ext } from 'generated/proto/command_deck_list_pb'; +import { Command_DeckNewDir_ext } from 'generated/proto/command_deck_new_dir_pb'; +import { Command_DeckUpload_ext } from 'generated/proto/command_deck_upload_pb'; +import { Command_ReplayDeleteMatch_ext } from 'generated/proto/command_replay_delete_match_pb'; +import { Command_ReplayGetCode_ext } from 'generated/proto/command_replay_get_code_pb'; +import { Command_ReplayList_ext } from 'generated/proto/command_replay_list_pb'; +import { Command_ReplayModifyMatch_ext } from 'generated/proto/command_replay_modify_match_pb'; +import { Command_ReplaySubmitCode_ext } from 'generated/proto/command_replay_submit_code_pb'; +import { Response_DeckList_ext } from 'generated/proto/response_deck_list_pb'; +import { Response_DeckUpload_ext } from 'generated/proto/response_deck_upload_pb'; +import { Response_GetGamesOfUser_ext } from 'generated/proto/response_get_games_of_user_pb'; +import { Response_GetUserInfo_ext } from 'generated/proto/response_get_user_info_pb'; +import { Response_JoinRoom_ext } from 'generated/proto/response_join_room_pb'; +import { Response_ListUsers_ext } from 'generated/proto/response_list_users_pb'; +import { Response_ReplayGetCode_ext } from 'generated/proto/response_replay_get_code_pb'; +import { Response_ReplayList_ext } from 'generated/proto/response_replay_list_pb'; import { accountEdit } from './accountEdit'; import { accountImage } from './accountImage'; import { accountPassword } from './accountPassword'; @@ -68,7 +95,8 @@ import { replayGetCode } from './replayGetCode'; import { replaySubmitCode } from './replaySubmitCode'; const { invokeOnSuccess, invokeCallback } = makeCallbackHelpers( - BackendService.sendSessionCommand as vi.Mock + BackendService.sendSessionCommand as vi.Mock, + 2 ); beforeEach(() => { @@ -86,8 +114,8 @@ describe('accountEdit', () => { it('sends Command_AccountEdit with correct params', () => { accountEdit('pw', 'Alice', 'a@b.com', 'US'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_AccountEdit', - { passwordCheck: 'pw', realName: 'Alice', email: 'a@b.com', country: 'US' }, + Command_AccountEdit_ext, + expect.objectContaining({ passwordCheck: 'pw', realName: 'Alice', email: 'a@b.com', country: 'US' }), expect.any(Object) ); }); @@ -105,7 +133,9 @@ describe('accountImage', () => { it('sends Command_AccountImage', () => { const img = new Uint8Array([1, 2]); accountImage(img); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_AccountImage', { image: img }, expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_AccountImage_ext, expect.objectContaining({ image: img }), expect.any(Object) + ); }); it('calls SessionPersistence.accountImageChanged on success', () => { @@ -122,8 +152,8 @@ describe('accountPassword', () => { it('sends Command_AccountPassword', () => { accountPassword('old', 'new', 'hashed'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_AccountPassword', - { oldPassword: 'old', newPassword: 'new', hashedNewPassword: 'hashed' }, + Command_AccountPassword_ext, + expect.objectContaining({ oldPassword: 'old', newPassword: 'new', hashedNewPassword: 'hashed' }), expect.any(Object) ); }); @@ -140,7 +170,11 @@ describe('deckDel', () => { it('sends Command_DeckDel', () => { deckDel(42); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_DeckDel', { deckId: 42 }, expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_DeckDel_ext, + expect.objectContaining({ deckId: 42 }), + expect.any(Object) + ); }); it('calls deleteServerDeck on success', () => { @@ -155,7 +189,9 @@ describe('deckDelDir', () => { it('sends Command_DeckDelDir', () => { deckDelDir('/path'); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_DeckDelDir', { path: '/path' }, expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_DeckDelDir_ext, expect.objectContaining({ path: '/path' }), expect.any(Object) + ); }); it('calls deleteServerDeckDir on success', () => { @@ -170,13 +206,17 @@ describe('deckList', () => { it('sends Command_DeckList', () => { deckList(); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_DeckList', {}, expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_DeckList_ext, + expect.any(Object), + expect.objectContaining({ responseExt: Response_DeckList_ext }) + ); }); it('calls updateServerDecks on success', () => { deckList(); const resp = { folders: [] }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_DeckList.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionPersistence.updateServerDecks).toHaveBeenCalledWith(resp); }); }); @@ -187,7 +227,7 @@ describe('deckNewDir', () => { it('sends Command_DeckNewDir', () => { deckNewDir('/path', 'dir'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_DeckNewDir', { path: '/path', dirName: 'dir' }, expect.any(Object) + Command_DeckNewDir_ext, expect.objectContaining({ path: '/path', dirName: 'dir' }), expect.any(Object) ); }); @@ -204,16 +244,16 @@ describe('deckUpload', () => { it('sends Command_DeckUpload', () => { deckUpload('/path', 1, 'content'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_DeckUpload', - { path: '/path', deckId: 1, deckList: 'content' }, - expect.any(Object) + Command_DeckUpload_ext, + expect.objectContaining({ path: '/path', deckId: 1, deckList: 'content' }), + expect.objectContaining({ responseExt: Response_DeckUpload_ext }) ); }); it('calls uploadServerDeck on success', () => { deckUpload('/path', 1, 'content'); const resp = { newFile: { id: 1 } }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_DeckUpload.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionPersistence.uploadServerDeck).toHaveBeenCalledWith('/path', resp.newFile); }); }); @@ -232,13 +272,17 @@ describe('getGamesOfUser', () => { it('sends Command_GetGamesOfUser', () => { getGamesOfUser('alice'); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_GetGamesOfUser', { userName: 'alice' }, expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_GetGamesOfUser_ext, + expect.any(Object), + expect.objectContaining({ responseExt: Response_GetGamesOfUser_ext }) + ); }); it('calls getGamesOfUser on success', () => { getGamesOfUser('alice'); const resp = { gameList: [] }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_GetGamesOfUser.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionPersistence.getGamesOfUser).toHaveBeenCalledWith('alice', resp); }); }); @@ -248,13 +292,17 @@ describe('getUserInfo', () => { it('sends Command_GetUserInfo', () => { getUserInfo('alice'); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_GetUserInfo', { userName: 'alice' }, expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_GetUserInfo_ext, + expect.any(Object), + expect.objectContaining({ responseExt: Response_GetUserInfo_ext }) + ); }); it('calls getUserInfo on success', () => { getUserInfo('alice'); const resp = { userInfo: { name: 'alice' } }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_GetUserInfo.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionPersistence.getUserInfo).toHaveBeenCalledWith(resp.userInfo); }); }); @@ -264,13 +312,17 @@ describe('joinRoom', () => { it('sends Command_JoinRoom', () => { joinRoom(5); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_JoinRoom', { roomId: 5 }, expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_JoinRoom_ext, + expect.any(Object), + expect.objectContaining({ responseExt: Response_JoinRoom_ext }) + ); }); it('calls RoomPersistence.joinRoom on success', () => { joinRoom(5); const resp = { roomInfo: { roomId: 5 } }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_JoinRoom.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(RoomPersistence.joinRoom).toHaveBeenCalledWith(resp.roomInfo); }); }); @@ -280,7 +332,7 @@ describe('listRooms (command)', () => { it('sends Command_ListRooms', () => { listRooms(); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ListRooms', {}, {}); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(Command_ListRooms_ext, expect.any(Object)); }); }); @@ -289,13 +341,17 @@ describe('listUsers', () => { it('sends Command_ListUsers', () => { listUsers(); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ListUsers', {}, expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_ListUsers_ext, + expect.any(Object), + expect.objectContaining({ responseExt: Response_ListUsers_ext }) + ); }); it('calls SessionPersistence.updateUsers with the user list on success', () => { listUsers(); const resp = { userList: [{ name: 'Alice' }] }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_ListUsers.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionPersistence.updateUsers).toHaveBeenCalledWith([{ name: 'Alice' }]); }); }); @@ -306,7 +362,7 @@ describe('message', () => { it('sends Command_Message', () => { message('bob', 'hi'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_Message', { userName: 'bob', message: 'hi' }, expect.any(Object) + Command_Message_ext, expect.objectContaining({ userName: 'bob', message: 'hi' }) ); }); @@ -318,7 +374,7 @@ describe('ping', () => { it('sends Command_Ping', () => { const pingReceived = vi.fn(); ping(pingReceived); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_Ping', {}, expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith(Command_Ping_ext, expect.any(Object), expect.any(Object)); }); it('calls pingReceived via onResponse', () => { @@ -335,7 +391,11 @@ describe('replayDeleteMatch', () => { it('sends Command_ReplayDeleteMatch', () => { replayDeleteMatch(7); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ReplayDeleteMatch', { gameId: 7 }, expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_ReplayDeleteMatch_ext, + expect.objectContaining({ gameId: 7 }), + expect.any(Object) + ); }); it('calls replayDeleteMatch on success', () => { @@ -350,13 +410,17 @@ describe('replayList', () => { it('sends Command_ReplayList', () => { replayList(); - expect(BackendService.sendSessionCommand).toHaveBeenCalledWith('Command_ReplayList', {}, expect.any(Object)); + expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( + Command_ReplayList_ext, + expect.any(Object), + expect.objectContaining({ responseExt: Response_ReplayList_ext }) + ); }); it('calls replayList on success', () => { replayList(); const resp = { matchList: [] }; - invokeOnSuccess(resp, { responseCode: 0, '.Response_ReplayList.ext': resp }); + invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionPersistence.replayList).toHaveBeenCalledWith([]); }); }); @@ -367,7 +431,7 @@ describe('replayModifyMatch', () => { it('sends Command_ReplayModifyMatch', () => { replayModifyMatch(7, true); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_ReplayModifyMatch', { gameId: 7, doNotHide: true }, expect.any(Object) + Command_ReplayModifyMatch_ext, expect.objectContaining({ gameId: 7, doNotHide: true }), expect.any(Object) ); }); @@ -384,14 +448,18 @@ describe('addToList / addToBuddyList / addToIgnoreList', () => { it('addToBuddyList sends Command_AddToList with list=buddy', () => { addToBuddyList('alice'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_AddToList', { list: 'buddy', userName: 'alice' }, expect.any(Object) + Command_AddToList_ext, + expect.objectContaining({ list: 'buddy' }), + expect.any(Object) ); }); it('addToIgnoreList sends Command_AddToList with list=ignore', () => { addToIgnoreList('bob'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_AddToList', { list: 'ignore', userName: 'bob' }, expect.any(Object) + Command_AddToList_ext, + expect.objectContaining({ list: 'ignore' }), + expect.any(Object) ); }); @@ -408,14 +476,18 @@ describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => { it('removeFromBuddyList sends Command_RemoveFromList with list=buddy', () => { removeFromBuddyList('alice'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_RemoveFromList', { list: 'buddy', userName: 'alice' }, expect.any(Object) + Command_RemoveFromList_ext, + expect.objectContaining({ list: 'buddy' }), + expect.any(Object) ); }); it('removeFromIgnoreList sends Command_RemoveFromList with list=ignore', () => { removeFromIgnoreList('bob'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_RemoveFromList', { list: 'ignore', userName: 'bob' }, expect.any(Object) + Command_RemoveFromList_ext, + expect.objectContaining({ list: 'ignore' }), + expect.any(Object) ); }); @@ -429,12 +501,12 @@ describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => { describe('replayGetCode', () => { beforeEach(() => vi.clearAllMocks()); - it('sends Command_ReplayGetCode with gameId and responseName', () => { + it('sends Command_ReplayGetCode with gameId and responseExt', () => { replayGetCode(42, vi.fn()); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_ReplayGetCode', - { gameId: 42 }, - expect.objectContaining({ responseName: 'Response_ReplayGetCode' }) + Command_ReplayGetCode_ext, + expect.any(Object), + expect.objectContaining({ responseExt: Response_ReplayGetCode_ext }) ); }); @@ -452,9 +524,7 @@ describe('replaySubmitCode', () => { it('sends Command_ReplaySubmitCode with replayCode', () => { replaySubmitCode('42-abc123'); expect(BackendService.sendSessionCommand).toHaveBeenCalledWith( - 'Command_ReplaySubmitCode', - { replayCode: '42-abc123' }, - expect.any(Object) + Command_ReplaySubmitCode_ext, expect.objectContaining({ replayCode: '42-abc123' }), expect.any(Object) ); }); diff --git a/webclient/src/websocket/events/common/commonEvents.spec.ts b/webclient/src/websocket/events/common/commonEvents.spec.ts index 7dd74c37d..cdfaa86dc 100644 --- a/webclient/src/websocket/events/common/commonEvents.spec.ts +++ b/webclient/src/websocket/events/common/commonEvents.spec.ts @@ -2,6 +2,6 @@ import { CommonEvents } from './index'; describe('CommonEvents', () => { it('is an empty event map (all common events were moved to game/session events)', () => { - expect(CommonEvents).toEqual({}); + expect(CommonEvents).toEqual([]); }); }); diff --git a/webclient/src/websocket/events/common/index.ts b/webclient/src/websocket/events/common/index.ts index 305171fbc..93d40e0e6 100644 --- a/webclient/src/websocket/events/common/index.ts +++ b/webclient/src/websocket/events/common/index.ts @@ -1,3 +1,3 @@ -import { ProtobufEvents } from '../../services/ProtobufService'; +import { ExtensionRegistry } from '../../services/ProtobufService'; -export const CommonEvents: ProtobufEvents = {}; +export const CommonEvents: ExtensionRegistry = []; diff --git a/webclient/src/websocket/events/game/index.ts b/webclient/src/websocket/events/game/index.ts index aa4be6391..64c26b153 100644 --- a/webclient/src/websocket/events/game/index.ts +++ b/webclient/src/websocket/events/game/index.ts @@ -1,4 +1,4 @@ -import { ProtobufEvents } from '../../services/ProtobufService'; +import { ExtensionRegistry } from '../../services/ProtobufService'; import { attachCard } from './attachCard'; import { changeZoneProperties } from './changeZoneProperties'; import { createArrow } from './createArrow'; @@ -29,34 +29,65 @@ import { setCardCounter } from './setCardCounter'; import { setCounter } from './setCounter'; import { shuffle } from './shuffle'; -export const GameEvents: ProtobufEvents = { - '.Event_Join.ext': joinGame, - '.Event_Leave.ext': leaveGame, - '.Event_GameClosed.ext': gameClosed, - '.Event_GameHostChanged.ext': gameHostChanged, - '.Event_Kicked.ext': kicked, - '.Event_GameStateChanged.ext': gameStateChanged, - '.Event_PlayerPropertiesChanged.ext': playerPropertiesChanged, - '.Event_GameSay.ext': gameSay, - '.Event_CreateArrow.ext': createArrow, - '.Event_DeleteArrow.ext': deleteArrow, - '.Event_CreateCounter.ext': createCounter, - '.Event_SetCounter.ext': setCounter, - '.Event_DelCounter.ext': delCounter, - '.Event_DrawCards.ext': drawCards, - '.Event_RevealCards.ext': revealCards, - '.Event_Shuffle.ext': shuffle, - '.Event_RollDie.ext': rollDie, - '.Event_MoveCard.ext': moveCard, - '.Event_FlipCard.ext': flipCard, - '.Event_DestroyCard.ext': destroyCard, - '.Event_AttachCard.ext': attachCard, - '.Event_CreateToken.ext': createToken, - '.Event_SetCardAttr.ext': setCardAttr, - '.Event_SetCardCounter.ext': setCardCounter, - '.Event_SetActivePlayer.ext': setActivePlayer, - '.Event_SetActivePhase.ext': setActivePhase, - '.Event_DumpZone.ext': dumpZone, - '.Event_ChangeZoneProperties.ext': changeZoneProperties, - '.Event_ReverseTurn.ext': reverseTurn, -}; +import { Event_Join_ext } from 'generated/proto/event_join_pb'; +import { Event_Leave_ext } from 'generated/proto/event_leave_pb'; +import { Event_GameClosed_ext } from 'generated/proto/event_game_closed_pb'; +import { Event_GameHostChanged_ext } from 'generated/proto/event_game_host_changed_pb'; +import { Event_Kicked_ext } from 'generated/proto/event_kicked_pb'; +import { Event_GameStateChanged_ext } from 'generated/proto/event_game_state_changed_pb'; +import { Event_PlayerPropertiesChanged_ext } from 'generated/proto/event_player_properties_changed_pb'; +import { Event_GameSay_ext } from 'generated/proto/event_game_say_pb'; +import { Event_CreateArrow_ext } from 'generated/proto/event_create_arrow_pb'; +import { Event_DeleteArrow_ext } from 'generated/proto/event_delete_arrow_pb'; +import { Event_CreateCounter_ext } from 'generated/proto/event_create_counter_pb'; +import { Event_SetCounter_ext } from 'generated/proto/event_set_counter_pb'; +import { Event_DelCounter_ext } from 'generated/proto/event_del_counter_pb'; +import { Event_DrawCards_ext } from 'generated/proto/event_draw_cards_pb'; +import { Event_RevealCards_ext } from 'generated/proto/event_reveal_cards_pb'; +import { Event_Shuffle_ext } from 'generated/proto/event_shuffle_pb'; +import { Event_RollDie_ext } from 'generated/proto/event_roll_die_pb'; +import { Event_MoveCard_ext } from 'generated/proto/event_move_card_pb'; +import { Event_FlipCard_ext } from 'generated/proto/event_flip_card_pb'; +import { Event_DestroyCard_ext } from 'generated/proto/event_destroy_card_pb'; +import { Event_AttachCard_ext } from 'generated/proto/event_attach_card_pb'; +import { Event_CreateToken_ext } from 'generated/proto/event_create_token_pb'; +import { Event_SetCardAttr_ext } from 'generated/proto/event_set_card_attr_pb'; +import { Event_SetCardCounter_ext } from 'generated/proto/event_set_card_counter_pb'; +import { Event_SetActivePlayer_ext } from 'generated/proto/event_set_active_player_pb'; +import { Event_SetActivePhase_ext } from 'generated/proto/event_set_active_phase_pb'; +import { Event_DumpZone_ext } from 'generated/proto/event_dump_zone_pb'; +import { Event_ChangeZoneProperties_ext } from 'generated/proto/event_change_zone_properties_pb'; +import { Event_ReverseTurn_ext } from 'generated/proto/event_reverse_turn_pb'; + +export const GameEvents: ExtensionRegistry = [ + [Event_Join_ext, joinGame], + [Event_Leave_ext, leaveGame], + [Event_GameClosed_ext, gameClosed], + [Event_GameHostChanged_ext, gameHostChanged], + [Event_Kicked_ext, kicked], + [Event_GameStateChanged_ext, gameStateChanged], + [Event_PlayerPropertiesChanged_ext, playerPropertiesChanged], + [Event_GameSay_ext, gameSay], + [Event_CreateArrow_ext, createArrow], + [Event_DeleteArrow_ext, deleteArrow], + [Event_CreateCounter_ext, createCounter], + [Event_SetCounter_ext, setCounter], + [Event_DelCounter_ext, delCounter], + [Event_DrawCards_ext, drawCards], + [Event_RevealCards_ext, revealCards], + [Event_Shuffle_ext, shuffle], + [Event_RollDie_ext, rollDie], + [Event_MoveCard_ext, moveCard], + [Event_FlipCard_ext, flipCard], + [Event_DestroyCard_ext, destroyCard], + [Event_AttachCard_ext, attachCard], + [Event_CreateToken_ext, createToken], + [Event_SetCardAttr_ext, setCardAttr], + [Event_SetCardCounter_ext, setCardCounter], + [Event_SetActivePlayer_ext, setActivePlayer], + [Event_SetActivePhase_ext, setActivePhase], + [Event_DumpZone_ext, dumpZone], + [Event_ChangeZoneProperties_ext, changeZoneProperties], + [Event_ReverseTurn_ext, reverseTurn], +]; + diff --git a/webclient/src/websocket/events/room/index.ts b/webclient/src/websocket/events/room/index.ts index 5b571d388..366d68476 100644 --- a/webclient/src/websocket/events/room/index.ts +++ b/webclient/src/websocket/events/room/index.ts @@ -1,4 +1,4 @@ -import { ProtobufEvents } from '../../services/ProtobufService'; +import { ExtensionRegistry } from '../../services/ProtobufService'; import { joinRoom } from './joinRoom'; import { leaveRoom } from './leaveRoom'; @@ -6,10 +6,17 @@ import { listGames } from './listGames'; import { roomSay } from './roomSay'; import { removeMessages } from './removeMessages'; -export const RoomEvents: ProtobufEvents = { - '.Event_JoinRoom.ext': joinRoom, - '.Event_LeaveRoom.ext': leaveRoom, - '.Event_ListGames.ext': listGames, - '.Event_RemoveMessages.ext': removeMessages, - '.Event_RoomSay.ext': roomSay, -}; +import { Event_JoinRoom_ext } from 'generated/proto/event_join_room_pb'; +import { Event_LeaveRoom_ext } from 'generated/proto/event_leave_room_pb'; +import { Event_ListGames_ext } from 'generated/proto/event_list_games_pb'; +import { Event_RemoveMessages_ext } from 'generated/proto/event_remove_messages_pb'; +import { Event_RoomSay_ext } from 'generated/proto/event_room_say_pb'; + +export const RoomEvents: ExtensionRegistry = [ + [Event_JoinRoom_ext, joinRoom], + [Event_LeaveRoom_ext, leaveRoom], + [Event_ListGames_ext, listGames], + [Event_RemoveMessages_ext, removeMessages], + [Event_RoomSay_ext, roomSay], +]; + diff --git a/webclient/src/websocket/events/room/interfaces.ts b/webclient/src/websocket/events/room/interfaces.ts index b3f922141..cb7086555 100644 --- a/webclient/src/websocket/events/room/interfaces.ts +++ b/webclient/src/websocket/events/room/interfaces.ts @@ -1,24 +1,11 @@ -import { Game, User } from 'types'; +import type { Event_JoinRoom } from 'generated/proto/event_join_room_pb'; +import type { Event_LeaveRoom } from 'generated/proto/event_leave_room_pb'; +import type { Event_ListGames } from 'generated/proto/event_list_games_pb'; +import type { Event_RemoveMessages } from 'generated/proto/event_remove_messages_pb'; +import type { RoomEvent as GeneratedRoomEvent } from 'generated/proto/room_event_pb'; -export interface JoinRoomData { - userInfo: User; -} - -export interface LeaveRoomData { - name: string; -} - -export interface ListGamesData { - gameList: Game[]; -} - -export interface RemoveMessagesData { - name: string; - amount: number; -} - -export interface RoomEvent { - roomEvent: { - roomId: number; - } -} +export type JoinRoomData = Event_JoinRoom; +export type LeaveRoomData = Event_LeaveRoom; +export type ListGamesData = Event_ListGames; +export type RemoveMessagesData = Event_RemoveMessages; +export type RoomEvent = GeneratedRoomEvent; diff --git a/webclient/src/websocket/events/room/joinRoom.ts b/webclient/src/websocket/events/room/joinRoom.ts index b1a5f6606..f45ff18ab 100644 --- a/webclient/src/websocket/events/room/joinRoom.ts +++ b/webclient/src/websocket/events/room/joinRoom.ts @@ -1,6 +1,6 @@ import { RoomPersistence } from '../../persistence'; import { JoinRoomData, RoomEvent } from './interfaces'; -export function joinRoom({ userInfo }: JoinRoomData, { roomEvent: { roomId } }: RoomEvent): void { +export function joinRoom({ userInfo }: JoinRoomData, { roomId }: RoomEvent): void { RoomPersistence.userJoined(roomId, userInfo); } diff --git a/webclient/src/websocket/events/room/leaveRoom.ts b/webclient/src/websocket/events/room/leaveRoom.ts index 6d45197fc..c7564d458 100644 --- a/webclient/src/websocket/events/room/leaveRoom.ts +++ b/webclient/src/websocket/events/room/leaveRoom.ts @@ -1,6 +1,6 @@ import { RoomPersistence } from '../../persistence'; import { LeaveRoomData, RoomEvent } from './interfaces'; -export function leaveRoom({ name }: LeaveRoomData, { roomEvent: { roomId } }: RoomEvent): void { +export function leaveRoom({ name }: LeaveRoomData, { roomId }: RoomEvent): void { RoomPersistence.userLeft(roomId, name); } diff --git a/webclient/src/websocket/events/room/listGames.ts b/webclient/src/websocket/events/room/listGames.ts index d460a5336..0f1f20438 100644 --- a/webclient/src/websocket/events/room/listGames.ts +++ b/webclient/src/websocket/events/room/listGames.ts @@ -1,6 +1,6 @@ import { RoomPersistence } from '../../persistence'; import { ListGamesData, RoomEvent } from './interfaces'; -export function listGames({ gameList }: ListGamesData, { roomEvent: { roomId } }: RoomEvent): void { +export function listGames({ gameList }: ListGamesData, { roomId }: RoomEvent): void { RoomPersistence.updateGames(roomId, gameList); } diff --git a/webclient/src/websocket/events/room/removeMessages.ts b/webclient/src/websocket/events/room/removeMessages.ts index 4fe01cb6f..859470c81 100644 --- a/webclient/src/websocket/events/room/removeMessages.ts +++ b/webclient/src/websocket/events/room/removeMessages.ts @@ -1,6 +1,6 @@ import { RoomPersistence } from '../../persistence'; import { RemoveMessagesData, RoomEvent } from './interfaces'; -export function removeMessages({ name, amount }: RemoveMessagesData, { roomEvent: { roomId } }: RoomEvent): void { +export function removeMessages({ name, amount }: RemoveMessagesData, { roomId }: RoomEvent): void { RoomPersistence.removeMessages(roomId, name, amount); } diff --git a/webclient/src/websocket/events/room/roomEvents.spec.ts b/webclient/src/websocket/events/room/roomEvents.spec.ts index c554128cc..f2e4eee8f 100644 --- a/webclient/src/websocket/events/room/roomEvents.spec.ts +++ b/webclient/src/websocket/events/room/roomEvents.spec.ts @@ -15,7 +15,7 @@ import { listGames } from './listGames'; import { removeMessages } from './removeMessages'; import { roomSay } from './roomSay'; -const makeRoomEvent = (roomId: number) => ({ roomEvent: { roomId } }); +const makeRoomEvent = (roomId: number) => ({ roomId }) as any; beforeEach(() => vi.clearAllMocks()); diff --git a/webclient/src/websocket/events/room/roomSay.ts b/webclient/src/websocket/events/room/roomSay.ts index 5a96198ea..9522248da 100644 --- a/webclient/src/websocket/events/room/roomSay.ts +++ b/webclient/src/websocket/events/room/roomSay.ts @@ -3,6 +3,6 @@ import { Message } from 'types'; import { RoomPersistence } from '../../persistence'; import { RoomEvent } from './interfaces'; -export function roomSay(message: Message, { roomEvent: { roomId } }: RoomEvent): void { +export function roomSay(message: Message, { roomId }: RoomEvent): void { RoomPersistence.addMessage(roomId, message); } diff --git a/webclient/src/websocket/events/session/connectionClosed.ts b/webclient/src/websocket/events/session/connectionClosed.ts index 796f3ab80..c98080f5f 100644 --- a/webclient/src/websocket/events/session/connectionClosed.ts +++ b/webclient/src/websocket/events/session/connectionClosed.ts @@ -1,5 +1,5 @@ import { StatusEnum } from 'types'; -import { ProtoController } from '../../services/ProtoController'; +import { Event_ConnectionClosed_CloseReason } from 'generated/proto/event_connection_closed_pb'; import { updateStatus } from '../../commands/session'; import { ConnectionClosedData } from './interfaces'; @@ -10,32 +10,31 @@ export function connectionClosed({ reason, reasonStr, endTime }: ConnectionClose if (reasonStr) { message = reasonStr; } else { - const { CloseReason } = ProtoController.root.Event_ConnectionClosed; switch (reason) { - case CloseReason.USER_LIMIT_REACHED: + case Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED: message = 'The server has reached its maximum user capacity'; break; - case CloseReason.TOO_MANY_CONNECTIONS: + case Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS: message = 'There are too many concurrent connections from your address'; break; - case CloseReason.BANNED: + case Event_ConnectionClosed_CloseReason.BANNED: message = typeof endTime === 'number' && endTime > 0 && Number.isFinite(endTime) ? `You are banned until ${new Date(endTime * 1000).toLocaleString()}` : 'You are banned'; break; - case CloseReason.DEMOTED: + case Event_ConnectionClosed_CloseReason.DEMOTED: message = 'You were demoted'; break; - case CloseReason.SERVER_SHUTDOWN: + case Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN: message = 'Scheduled server shutdown'; break; - case CloseReason.USERNAMEINVALID: + case Event_ConnectionClosed_CloseReason.USERNAMEINVALID: message = 'Invalid username'; break; - case CloseReason.LOGGEDINELSEWERE: + case Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE: message = 'You have been logged out due to logging in at another location'; break; - case CloseReason.OTHER: + case Event_ConnectionClosed_CloseReason.OTHER: default: message = 'Unknown reason'; break; diff --git a/webclient/src/websocket/events/session/index.ts b/webclient/src/websocket/events/session/index.ts index 5b3ab198e..6c34f0dfd 100644 --- a/webclient/src/websocket/events/session/index.ts +++ b/webclient/src/websocket/events/session/index.ts @@ -1,4 +1,4 @@ -import { ProtobufEvents } from '../../services/ProtobufService'; +import { ExtensionRegistry } from '../../services/ProtobufService'; import { addToList } from './addToList'; import { connectionClosed } from './connectionClosed'; import { listRooms } from './listRooms'; @@ -14,19 +14,35 @@ import { userLeft } from './userLeft'; import { userMessage } from './userMessage'; import { gameJoined } from './gameJoined'; -export const SessionEvents: ProtobufEvents = { - '.Event_AddToList.ext': addToList, - '.Event_ConnectionClosed.ext': connectionClosed, - '.Event_GameJoined.ext': gameJoined, - '.Event_ListRooms.ext': listRooms, - '.Event_NotifyUser.ext': notifyUser, - '.Event_RemoveFromList.ext': removeFromList, - '.Event_ReplayAdded.ext': replayAdded, - '.Event_ServerCompleteList.ext': serverCompleteList, - '.Event_ServerIdentification.ext': serverIdentification, - '.Event_ServerMessage.ext': serverMessage, - '.Event_ServerShutdown.ext': serverShutdown, - '.Event_UserJoined.ext': userJoined, - '.Event_UserLeft.ext': userLeft, - '.Event_UserMessage.ext': userMessage, -} +import { Event_AddToList_ext } from 'generated/proto/event_add_to_list_pb'; +import { Event_ConnectionClosed_ext } from 'generated/proto/event_connection_closed_pb'; +import { Event_GameJoined_ext } from 'generated/proto/event_game_joined_pb'; +import { Event_ListRooms_ext } from 'generated/proto/event_list_rooms_pb'; +import { Event_NotifyUser_ext } from 'generated/proto/event_notify_user_pb'; +import { Event_RemoveFromList_ext } from 'generated/proto/event_remove_from_list_pb'; +import { Event_ReplayAdded_ext } from 'generated/proto/event_replay_added_pb'; +import { Event_ServerCompleteList_ext } from 'generated/proto/event_server_complete_list_pb'; +import { Event_ServerIdentification_ext } from 'generated/proto/event_server_identification_pb'; +import { Event_ServerMessage_ext } from 'generated/proto/event_server_message_pb'; +import { Event_ServerShutdown_ext } from 'generated/proto/event_server_shutdown_pb'; +import { Event_UserJoined_ext } from 'generated/proto/event_user_joined_pb'; +import { Event_UserLeft_ext } from 'generated/proto/event_user_left_pb'; +import { Event_UserMessage_ext } from 'generated/proto/event_user_message_pb'; + +export const SessionEvents: ExtensionRegistry = [ + [Event_AddToList_ext, addToList], + [Event_ConnectionClosed_ext, connectionClosed], + [Event_GameJoined_ext, gameJoined], + [Event_ListRooms_ext, listRooms], + [Event_NotifyUser_ext, notifyUser], + [Event_RemoveFromList_ext, removeFromList], + [Event_ReplayAdded_ext, replayAdded], + [Event_ServerCompleteList_ext, serverCompleteList], + [Event_ServerIdentification_ext, serverIdentification], + [Event_ServerMessage_ext, serverMessage], + [Event_ServerShutdown_ext, serverShutdown], + [Event_UserJoined_ext, userJoined], + [Event_UserLeft_ext, userLeft], + [Event_UserMessage_ext, userMessage], +]; + diff --git a/webclient/src/websocket/events/session/interfaces.ts b/webclient/src/websocket/events/session/interfaces.ts index ddc10d103..fcd1fd1b7 100644 --- a/webclient/src/websocket/events/session/interfaces.ts +++ b/webclient/src/websocket/events/session/interfaces.ts @@ -1,90 +1,31 @@ -import { Game, NotificationType, ReplayMatch, Room, User } from 'types'; +import type { Event_AddToList } from 'generated/proto/event_add_to_list_pb'; +import type { Event_ConnectionClosed } from 'generated/proto/event_connection_closed_pb'; +import type { Event_GameJoined } from 'generated/proto/event_game_joined_pb'; +import type { Event_ListRooms } from 'generated/proto/event_list_rooms_pb'; +import type { Event_NotifyUser } from 'generated/proto/event_notify_user_pb'; +import type { Event_RemoveFromList } from 'generated/proto/event_remove_from_list_pb'; +import type { Event_ReplayAdded } from 'generated/proto/event_replay_added_pb'; +import type { Event_ServerCompleteList } from 'generated/proto/event_server_complete_list_pb'; +import type { Event_ServerIdentification } from 'generated/proto/event_server_identification_pb'; +import type { Event_ServerMessage } from 'generated/proto/event_server_message_pb'; +import type { Event_ServerShutdown } from 'generated/proto/event_server_shutdown_pb'; +import type { Event_UserJoined } from 'generated/proto/event_user_joined_pb'; +import type { Event_UserLeft } from 'generated/proto/event_user_left_pb'; +import type { Event_UserMessage } from 'generated/proto/event_user_message_pb'; +import type { Event_PlayerPropertiesChanged } from 'generated/proto/event_player_properties_changed_pb'; -export interface AddToListData { - listName: string; - userInfo: User; -} - -export interface ConnectionClosedData { - endTime: number; - reason: number; - reasonStr: string; -} - -export interface GameJoinedData { - gameInfo: Game; - gameTypes: any[]; - hostId: number; - playerId: number; - spectator: boolean; - resuming: boolean; - judge: boolean; -} - -export interface ListRoomsData { - roomList: Room[]; -} - -export interface NotifyUserData { - type: NotificationType; - warningReason: string; - customTitle: string; - customContent: string; -} - -export interface PlayerGamePropertiesData { - playerId: number; - userInfo: User; - spectator: boolean; - conceded: boolean; - readyStart: boolean; - deckHash: string; - pingSeconds: number; - sideboardLocked: boolean; - judge: boolean; -} - -export interface RemoveFromListData { - listName: string; - userName: string; -} - -export interface ServerIdentificationData { - protocolVersion: number; - serverName: string; - serverVersion: string; - serverOptions: number; -} - -export interface ServerMessageData { - message: string; -} - -export interface ServerShutdownData { - reason: string; - minutes: number; -} - -export interface UserJoinedData { - userInfo: User; -} - -export interface UserLeftData { - name: string; -} - -export interface UserMessageData { - senderName: string; - receiverName: string; - message: string; -} - -export interface ReplayAddedData { - matchInfo: ReplayMatch; -} - -export interface ServerCompleteListData { - serverId: number; - userList: User[]; - roomList: Room[]; -} +export type AddToListData = Event_AddToList; +export type ConnectionClosedData = Event_ConnectionClosed; +export type GameJoinedData = Event_GameJoined; +export type ListRoomsData = Event_ListRooms; +export type NotifyUserData = Event_NotifyUser; +export type RemoveFromListData = Event_RemoveFromList; +export type ReplayAddedData = Event_ReplayAdded; +export type ServerCompleteListData = Event_ServerCompleteList; +export type ServerIdentificationData = Event_ServerIdentification; +export type ServerMessageData = Event_ServerMessage; +export type ServerShutdownData = Event_ServerShutdown; +export type UserJoinedData = Event_UserJoined; +export type UserLeftData = Event_UserLeft; +export type UserMessageData = Event_UserMessage; +export type PlayerGamePropertiesData = Event_PlayerPropertiesChanged; diff --git a/webclient/src/websocket/events/session/sessionEvents.spec.ts b/webclient/src/websocket/events/session/sessionEvents.spec.ts index 612f4b497..fae54fc4b 100644 --- a/webclient/src/websocket/events/session/sessionEvents.spec.ts +++ b/webclient/src/websocket/events/session/sessionEvents.spec.ts @@ -51,26 +51,8 @@ vi.mock('../../utils', () => ({ passwordSaltSupported: vi.fn().mockReturnValue(0), })); -vi.mock('../../services/ProtoController', () => ({ - ProtoController: { - root: { - Event_ConnectionClosed: { - CloseReason: { - USER_LIMIT_REACHED: 0, - TOO_MANY_CONNECTIONS: 1, - BANNED: 2, - DEMOTED: 3, - SERVER_SHUTDOWN: 4, - USERNAMEINVALID: 5, - LOGGEDINELSEWERE: 6, - OTHER: 7, - }, - }, - }, - }, -})); - import { WebSocketConnectReason } from 'types'; +import { Event_ConnectionClosed_CloseReason } from 'generated/proto/event_connection_closed_pb'; import { SessionPersistence, RoomPersistence } from '../../persistence'; import webClient from '../../WebClient'; @@ -282,7 +264,7 @@ describe('connectionClosed', () => { }); it('USER_LIMIT_REACHED → specific message', () => { - connectionClosed({ reason: 0 } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith( expect.anything(), expect.stringContaining('maximum user capacity') @@ -290,42 +272,42 @@ describe('connectionClosed', () => { }); it('TOO_MANY_CONNECTIONS → specific message', () => { - connectionClosed({ reason: 1 } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('too many concurrent')); }); it('BANNED → specific message', () => { - connectionClosed({ reason: 2 } as any); - expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('banned')); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.BANNED } as any); + expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('DEMOTED → specific message', () => { - connectionClosed({ reason: 3 } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.DEMOTED } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('demoted')); }); it('SERVER_SHUTDOWN → specific message', () => { - connectionClosed({ reason: 4 } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('shutdown')); }); it('USERNAMEINVALID → specific message', () => { - connectionClosed({ reason: 5 } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.USERNAMEINVALID } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('username')); }); it('LOGGEDINELSEWERE → specific message', () => { - connectionClosed({ reason: 6 } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('logged out')); }); it('OTHER → "Unknown reason"', () => { - connectionClosed({ reason: 7 } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.OTHER } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'Unknown reason'); }); it('BANNED with valid positive endTime → shows formatted date', () => { - connectionClosed({ reason: 2, endTime: 1700000000 } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 1700000000 } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith( expect.anything(), expect.stringContaining('You are banned until') @@ -333,27 +315,27 @@ describe('connectionClosed', () => { }); it('BANNED with endTime = 0 → shows generic banned message', () => { - connectionClosed({ reason: 2, endTime: 0 } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 0 } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with endTime = -1 → shows generic banned message', () => { - connectionClosed({ reason: 2, endTime: -1 } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: -1 } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with endTime = NaN → shows generic banned message', () => { - connectionClosed({ reason: 2, endTime: NaN } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: NaN } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with endTime = Infinity → shows generic banned message', () => { - connectionClosed({ reason: 2, endTime: Infinity } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: Infinity } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with reasonStr → uses reasonStr regardless of endTime', () => { - connectionClosed({ reason: 2, endTime: 0, reasonStr: 'custom ban reason' } as any); + connectionClosed({ reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 0, reasonStr: 'custom ban reason' } as any); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'custom ban reason'); }); }); diff --git a/webclient/src/websocket/persistence/SessionPersistence.ts b/webclient/src/websocket/persistence/SessionPersistence.ts index 10fcc0539..6e96005ac 100644 --- a/webclient/src/websocket/persistence/SessionPersistence.ts +++ b/webclient/src/websocket/persistence/SessionPersistence.ts @@ -11,6 +11,10 @@ import { UserMessageData } from '../events/session/interfaces'; import NormalizeService from '../utils/NormalizeService'; +import type { Response_GetGamesOfUser } from 'generated/proto/response_get_games_of_user_pb'; +import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb'; +import type { ServerInfo_GameType } from 'generated/proto/serverinfo_gametype_pb'; +import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb'; export class SessionPersistence { static initialized() { @@ -171,14 +175,14 @@ export class SessionPersistence { ServerDispatch.getUserInfo(userInfo); } - static getGamesOfUser(userName: string, response: any): void { + static getGamesOfUser(userName: string, response: Response_GetGamesOfUser): void { const gametypeMap: Record = {}; - (response.roomList || []).forEach((room: any) => { - (room.gametypeList || []).forEach((gt: any) => { + (response.roomList || []).forEach((room: ServerInfo_Room) => { + (room.gametypeList || []).forEach((gt: ServerInfo_GameType) => { gametypeMap[gt.gameTypeId] = gt.description; }); }); - const games = (response.gameList || []).map((game: any) => { + const games = (response.gameList || []).map((game: ServerInfo_Game) => { NormalizeService.normalizeGameObject(game, gametypeMap); return game; }); diff --git a/webclient/src/websocket/services/BackendService.spec.ts b/webclient/src/websocket/services/BackendService.spec.ts index 85545f3cf..98d072338 100644 --- a/webclient/src/websocket/services/BackendService.spec.ts +++ b/webclient/src/websocket/services/BackendService.spec.ts @@ -1,7 +1,11 @@ -import { makeMockProtoRoot } from '../__mocks__/helpers'; +vi.mock('@bufbuild/protobuf', () => ({ + create: vi.fn().mockReturnValue({}), + setExtension: vi.fn(), + getExtension: vi.fn(), +})); -vi.mock('./ProtoController', () => ({ - ProtoController: { root: null }, +vi.mock('generated/proto/response_pb', () => ({ + Response_ResponseCode: { RespOk: 1 }, })); vi.mock('../WebClient', () => { @@ -15,35 +19,29 @@ vi.mock('../WebClient', () => { return { __esModule: true, default: { protobuf: mockProtobuf } }; }); +import { getExtension } from '@bufbuild/protobuf'; import { BackendService } from './BackendService'; -import { ProtoController } from './ProtoController'; import webClient from '../WebClient'; beforeEach(() => { vi.clearAllMocks(); - ProtoController.root = makeMockProtoRoot(); - ProtoController.root.GameCommand = { create: vi.fn(args => ({ ...args })) }; - ProtoController.root['Command_Game'] = { create: vi.fn(p => ({ ...p })) }; - ProtoController.root['Command_Test'] = { create: vi.fn(p => ({ ...p })) }; - ProtoController.root['Command_Room'] = { create: vi.fn(p => ({ ...p })) }; - ProtoController.root['Command_Mod'] = { create: vi.fn(p => ({ ...p })) }; - ProtoController.root['Command_Admin'] = { create: vi.fn(p => ({ ...p })) }; - ProtoController.root['Response_Test'] = {}; }); -function captureCallback(sendFn: vi.Mock) { - return sendFn.mock.calls[0][sendFn === (webClient.protobuf as any).sendRoomCommand ? 2 : 1]; +function captureCallback(sendFn: ReturnType) { + const protobuf = webClient.protobuf as any; + const usesIndex2 = sendFn === protobuf.sendRoomCommand || sendFn === protobuf.sendGameCommand; + return sendFn.mock.calls[0][usesIndex2 ? 2 : 1]; } describe('BackendService', () => { describe('send commands', () => { it.each([ - ['sendGameCommand', () => BackendService.sendGameCommand(7, 'Command_Game', { g: 1 })], - ['sendSessionCommand', () => BackendService.sendSessionCommand('Command_Test', { x: 1 }, {})], - ['sendRoomCommand', () => BackendService.sendRoomCommand(5, 'Command_Room', { y: 2 }, {})], - ['sendModeratorCommand', () => BackendService.sendModeratorCommand('Command_Mod', { z: 3 }, {})], - ['sendAdminCommand', () => BackendService.sendAdminCommand('Command_Admin', {}, {})], - ])('%s creates the command and delegates to protobuf', (methodName, invoke) => { + ['sendGameCommand', () => BackendService.sendGameCommand(7, {} as any, {} as any)], + ['sendSessionCommand', () => BackendService.sendSessionCommand({} as any, {} as any)], + ['sendRoomCommand', () => BackendService.sendRoomCommand(5, {} as any, {} as any)], + ['sendModeratorCommand', () => BackendService.sendModeratorCommand({} as any, {} as any)], + ['sendAdminCommand', () => BackendService.sendAdminCommand({} as any, {} as any)], + ])('%s delegates to protobuf', (methodName, invoke) => { invoke(); expect((webClient.protobuf as any)[methodName]).toHaveBeenCalled(); }); @@ -52,37 +50,37 @@ describe('BackendService', () => { describe('handleResponse via non-session command callbacks', () => { it('sendGameCommand callback invokes handleResponse', () => { const onSuccess = vi.fn(); - BackendService.sendGameCommand(7, 'Command_Game', {}, { onSuccess }); + BackendService.sendGameCommand(7, {} as any, {} as any, { onSuccess }); const cb = (webClient.protobuf as any).sendGameCommand.mock.calls[0][2]; - cb({ responseCode: 0 }); + cb({ responseCode: 1 }); expect(onSuccess).toHaveBeenCalled(); }); it('sendRoomCommand callback invokes handleResponse', () => { const onSuccess = vi.fn(); - BackendService.sendRoomCommand(5, 'Command_Room', {}, { onSuccess }); - captureCallback((webClient.protobuf as any).sendRoomCommand)({ responseCode: 0 }); + BackendService.sendRoomCommand(5, {} as any, {} as any, { onSuccess }); + captureCallback((webClient.protobuf as any).sendRoomCommand)({ responseCode: 1 }); expect(onSuccess).toHaveBeenCalled(); }); it('sendModeratorCommand callback invokes handleResponse', () => { const onSuccess = vi.fn(); - BackendService.sendModeratorCommand('Command_Mod', {}, { onSuccess }); - captureCallback((webClient.protobuf as any).sendModeratorCommand)({ responseCode: 0 }); + BackendService.sendModeratorCommand({} as any, {} as any, { onSuccess }); + captureCallback((webClient.protobuf as any).sendModeratorCommand)({ responseCode: 1 }); expect(onSuccess).toHaveBeenCalled(); }); it('sendAdminCommand callback invokes handleResponse', () => { const onSuccess = vi.fn(); - BackendService.sendAdminCommand('Command_Admin', {}, { onSuccess }); - captureCallback((webClient.protobuf as any).sendAdminCommand)({ responseCode: 0 }); + BackendService.sendAdminCommand({} as any, {} as any, { onSuccess }); + captureCallback((webClient.protobuf as any).sendAdminCommand)({ responseCode: 1 }); expect(onSuccess).toHaveBeenCalled(); }); }); describe('handleResponse (via sendSessionCommand callback)', () => { function invokeCallback(options: any, raw: any) { - BackendService.sendSessionCommand('Command_Test', {}, options); + BackendService.sendSessionCommand({} as any, {} as any, options); const cb = (webClient.protobuf as any).sendSessionCommand.mock.calls[0][1]; cb(raw); } @@ -95,17 +93,19 @@ describe('BackendService', () => { expect(onSuccess).not.toHaveBeenCalled(); }); - it('calls onSuccess with raw when responseCode is RespOk and no responseName', () => { + it('calls onSuccess with raw when responseCode is RespOk and no responseExt', () => { const onSuccess = vi.fn(); - const raw = { responseCode: 0 }; + const raw = { responseCode: 1 }; invokeCallback({ onSuccess }, raw); expect(onSuccess).toHaveBeenCalledWith(raw, raw); }); - it('calls onSuccess with nested response when responseName is set', () => { + it('calls onSuccess with nested response when responseExt is set', () => { + vi.mocked(getExtension).mockReturnValue({ nested: true }); const onSuccess = vi.fn(); - const raw = { responseCode: 0, '.Response_Test.ext': { nested: true } }; - invokeCallback({ onSuccess, responseName: 'Response_Test' }, raw); + const fakeExt = {} as any; + const raw = { responseCode: 1 }; + invokeCallback({ onSuccess, responseExt: fakeExt }, raw); expect(onSuccess).toHaveBeenCalledWith({ nested: true }, raw); }); diff --git a/webclient/src/websocket/services/BackendService.ts b/webclient/src/websocket/services/BackendService.ts index 94319c47e..c9b65eb44 100644 --- a/webclient/src/websocket/services/BackendService.ts +++ b/webclient/src/websocket/services/BackendService.ts @@ -1,66 +1,64 @@ -import webClient from '../WebClient'; -import { ProtoController } from './ProtoController'; +import { create, getExtension, setExtension } from '@bufbuild/protobuf'; +import type { GenExtension } from '@bufbuild/protobuf'; -export interface CommandOptions { - responseName?: string; - onSuccess?: (response: any, raw: any) => void; - onError?: (responseCode: number, raw: any) => void; - onResponseCode?: { [code: number]: (raw: any) => void }; - onResponse?: (raw: any) => void; +import webClient from '../WebClient'; +import { Response_ResponseCode, type Response } from 'generated/proto/response_pb'; +import { SessionCommandSchema, type SessionCommand } from 'generated/proto/session_commands_pb'; +import { GameCommandSchema, type GameCommand } from 'generated/proto/game_commands_pb'; +import { RoomCommandSchema, type RoomCommand } from 'generated/proto/room_commands_pb'; +import { ModeratorCommandSchema, type ModeratorCommand } from 'generated/proto/moderator_commands_pb'; +import { AdminCommandSchema, type AdminCommand } from 'generated/proto/admin_commands_pb'; + +export interface CommandOptions { + responseExt?: GenExtension; + onSuccess?: (response: R, raw: Response) => void; + onError?: (responseCode: number, raw: Response) => void; + onResponseCode?: { [code: number]: (raw: Response) => void }; + onResponse?: (raw: Response) => void; } export class BackendService { - static sendGameCommand(gameId: number, commandName: string, params: any, options: CommandOptions = {}): void { - const command = ProtoController.root[commandName].create(params || {}); - const gc = ProtoController.root.GameCommand.create({ - [`.${commandName}.ext`]: command, - }); - webClient.protobuf.sendGameCommand(gameId, gc, (raw: any) => { - BackendService.handleResponse(commandName, raw, options); + static sendGameCommand(gameId: number, ext: GenExtension, value: V, options: CommandOptions = {}): void { + const cmd = create(GameCommandSchema); + setExtension(cmd, ext, value); + webClient.protobuf.sendGameCommand(gameId, cmd, (raw: Response) => { + BackendService.handleResponse(ext, raw, options); }); } - static sendSessionCommand(commandName: string, params: any, options: CommandOptions): void { - const command = ProtoController.root[commandName].create(params || {}); - const sc = ProtoController.root.SessionCommand.create({ - [`.${commandName}.ext`]: command, - }); - webClient.protobuf.sendSessionCommand(sc, raw => { - BackendService.handleResponse(commandName, raw, options); + static sendSessionCommand(ext: GenExtension, value: V, options: CommandOptions = {}): void { + const cmd = create(SessionCommandSchema); + setExtension(cmd, ext, value); + webClient.protobuf.sendSessionCommand(cmd, raw => { + BackendService.handleResponse(ext, raw, options); }); } - static sendRoomCommand(roomId: number, commandName: string, params: any, options: CommandOptions): void { - const command = ProtoController.root[commandName].create(params || {}); - const rc = ProtoController.root.RoomCommand.create({ - [`.${commandName}.ext`]: command, - }); - webClient.protobuf.sendRoomCommand(roomId, rc, raw => { - BackendService.handleResponse(commandName, raw, options); + static sendRoomCommand(roomId: number, ext: GenExtension, value: V, options: CommandOptions = {}): void { + const cmd = create(RoomCommandSchema); + setExtension(cmd, ext, value); + webClient.protobuf.sendRoomCommand(roomId, cmd, raw => { + BackendService.handleResponse(ext, raw, options); }); } - static sendModeratorCommand(commandName: string, params: any, options: CommandOptions): void { - const command = ProtoController.root[commandName].create(params || {}); - const mc = ProtoController.root.ModeratorCommand.create({ - [`.${commandName}.ext`]: command, - }); - webClient.protobuf.sendModeratorCommand(mc, raw => { - BackendService.handleResponse(commandName, raw, options); + static sendModeratorCommand(ext: GenExtension, value: V, options: CommandOptions = {}): void { + const cmd = create(ModeratorCommandSchema); + setExtension(cmd, ext, value); + webClient.protobuf.sendModeratorCommand(cmd, raw => { + BackendService.handleResponse(ext, raw, options); }); } - static sendAdminCommand(commandName: string, params: any, options: CommandOptions): void { - const command = ProtoController.root[commandName].create(params || {}); - const ac = ProtoController.root.AdminCommand.create({ - [`.${commandName}.ext`]: command, - }); - webClient.protobuf.sendAdminCommand(ac, raw => { - BackendService.handleResponse(commandName, raw, options); + static sendAdminCommand(ext: GenExtension, value: V, options: CommandOptions = {}): void { + const cmd = create(AdminCommandSchema); + setExtension(cmd, ext, value); + webClient.protobuf.sendAdminCommand(cmd, raw => { + BackendService.handleResponse(ext, raw, options); }); } - private static handleResponse(commandName: string, raw: any, options: CommandOptions): void { + private static handleResponse(ext: GenExtension, raw: Response, options: CommandOptions): void { if (options.onResponse) { options.onResponse(raw); return; @@ -68,11 +66,11 @@ export class BackendService { const { responseCode } = raw; - if (responseCode === ProtoController.root.Response.ResponseCode.RespOk) { + if (responseCode === Response_ResponseCode.RespOk) { if (options.onSuccess) { - const response = options.responseName - ? raw[`.${options.responseName}.ext`] - : raw; + const response = options.responseExt + ? getExtension(raw, options.responseExt) + : raw as unknown as R; options.onSuccess(response, raw); } return; @@ -86,7 +84,8 @@ export class BackendService { if (options.onError) { options.onError(responseCode, raw); } else { - console.error(`${commandName} failed with response code: ${responseCode}`); + console.error(`${ext.typeName} failed with response code: ${responseCode}`); } } } + diff --git a/webclient/src/websocket/services/ProtoController.spec.ts b/webclient/src/websocket/services/ProtoController.spec.ts deleted file mode 100644 index b16b3bedb..000000000 --- a/webclient/src/websocket/services/ProtoController.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -vi.mock('../persistence', () => ({ - SessionPersistence: { initialized: vi.fn() }, -})); - -vi.mock('../../proto-files.json', () => ({ default: ['test.proto'] })); - -import { ProtoController } from './ProtoController'; -import { SessionPersistence } from '../persistence'; -import protobuf from 'protobufjs'; - -beforeEach(() => { - vi.clearAllMocks(); - ProtoController.root = null; -}); - -describe('ProtoController', () => { - describe('load', () => { - it('creates a new protobuf.Root', () => { - ProtoController.load(); - expect(ProtoController.root).toBeDefined(); - }); - - it('calls initialized when callback succeeds', () => { - const loadSpy = vi.spyOn(protobuf.Root.prototype, 'load').mockImplementation( - ((_files: any, _opts: any, cb: any) => cb(null)) as any - ); - ProtoController.load(); - expect(SessionPersistence.initialized).toHaveBeenCalled(); - loadSpy.mockRestore(); - }); - - it('throws when callback receives an error', () => { - const loadSpy = vi.spyOn(protobuf.Root.prototype, 'load').mockImplementation( - ((_files: any, _opts: any, cb: any) => cb(new Error('load failed'))) as any - ); - expect(() => ProtoController.load()).toThrow('load failed'); - loadSpy.mockRestore(); - }); - }); -}); diff --git a/webclient/src/websocket/services/ProtoController.ts b/webclient/src/websocket/services/ProtoController.ts deleted file mode 100644 index 4f6b37a19..000000000 --- a/webclient/src/websocket/services/ProtoController.ts +++ /dev/null @@ -1,24 +0,0 @@ -import protobuf from 'protobufjs'; - -import { SessionPersistence } from '../persistence'; -import ProtoFiles from '../../proto-files.json'; - -const PB_FILE_DIR = `${import.meta.env.BASE_URL}pb`; - -// Leaf module — no imports from the websocket layer other than persistence. -// Both BackendService and ProtobufService import this; neither should import -// the other for controller access, avoiding circular dependency cycles. -export const ProtoController = { - root: null as any, - - load(): void { - const files = ProtoFiles.map(file => `${PB_FILE_DIR}/${file}`); - ProtoController.root = new protobuf.Root(); - ProtoController.root.load(files, { keepCase: false }, (err: Error) => { - if (err) { - throw err; - } - SessionPersistence.initialized(); - }); - }, -}; diff --git a/webclient/src/websocket/services/ProtobufService.spec.ts b/webclient/src/websocket/services/ProtobufService.spec.ts index 5e933f118..2e1913e59 100644 --- a/webclient/src/websocket/services/ProtobufService.spec.ts +++ b/webclient/src/websocket/services/ProtobufService.spec.ts @@ -1,7 +1,23 @@ -import { makeMockProtoRoot } from '../__mocks__/helpers'; +vi.mock('@bufbuild/protobuf', () => ({ + create: vi.fn((_schema: any, fields?: any) => ({ ...(fields ?? {}) })), + fromBinary: vi.fn(), + toBinary: vi.fn().mockReturnValue(new Uint8Array()), + hasExtension: vi.fn().mockReturnValue(false), + getExtension: vi.fn(), +})); -vi.mock('./ProtoController', () => ({ - ProtoController: { root: null, load: vi.fn() }, +vi.mock('generated/proto/commands_pb', () => ({ + CommandContainerSchema: {}, +})); + +vi.mock('generated/proto/server_message_pb', () => ({ + ServerMessageSchema: {}, + ServerMessage_MessageType: { + RESPONSE: 1, + ROOM_EVENT: 2, + SESSION_EVENT: 3, + GAME_EVENT_CONTAINER: 4, + }, })); vi.mock('../commands/session', () => ({ @@ -10,9 +26,9 @@ vi.mock('../commands/session', () => ({ })); vi.mock('../events', () => ({ - GameEvents: { '.Event_Game.ext': vi.fn() }, - RoomEvents: { '.Event_Room.ext': vi.fn() }, - SessionEvents: { '.Event_Session.ext': vi.fn() }, + GameEvents: [], + RoomEvents: [], + SessionEvents: [], })); vi.mock('../WebClient', () => ({ @@ -20,42 +36,28 @@ vi.mock('../WebClient', () => ({ default: {}, })); +import { fromBinary, toBinary, hasExtension, getExtension } from '@bufbuild/protobuf'; +import { ServerMessage_MessageType } from 'generated/proto/server_message_pb'; import { ProtobufService } from './ProtobufService'; -import { ProtoController } from './ProtoController'; import { ping as sessionPing } from '../commands/session'; import { GameEvents } from '../events'; let mockSocket: any; -let mockWebClient: any; beforeEach(() => { vi.clearAllMocks(); - ProtoController.root = makeMockProtoRoot(); - const encodeResult = { finish: vi.fn().mockReturnValue(new Uint8Array([1, 2])) }; - ProtoController.root.CommandContainer.encode = vi.fn().mockReturnValue(encodeResult); - mockSocket = { checkReadyState: vi.fn().mockReturnValue(true), send: vi.fn(), }; - - mockWebClient = { - socket: mockSocket, - }; }); describe('ProtobufService', () => { - it('calls ProtoController.load on construction', () => { - new ProtobufService(mockWebClient); - expect(ProtoController.load).toHaveBeenCalled(); - }); - describe('resetCommands', () => { it('resets cmdId and pendingCommands', () => { - const service = new ProtobufService(mockWebClient); - // add a pending command - service.sendSessionCommand({}, vi.fn()); + const service = new ProtobufService({ socket: mockSocket } as any); + service.sendSessionCommand({} as any, vi.fn()); expect((service as any).cmdId).toBe(1); service.resetCommands(); expect((service as any).cmdId).toBe(0); @@ -65,42 +67,41 @@ describe('ProtobufService', () => { describe('sendCommand', () => { it('increments cmdId and stores callback', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const cb = vi.fn(); - service.sendCommand({}, cb); + service.sendCommand({} as any, cb); expect((service as any).cmdId).toBe(1); expect((service as any).pendingCommands[1]).toBe(cb); }); it('sends encoded data when socket is OPEN', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); mockSocket.checkReadyState.mockReturnValue(true); - service.sendCommand({}, vi.fn()); + service.sendCommand({} as any, vi.fn()); expect(mockSocket.send).toHaveBeenCalled(); }); it('does not send when socket is not OPEN', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); mockSocket.checkReadyState.mockReturnValue(false); - service.sendCommand({}, vi.fn()); + service.sendCommand({} as any, vi.fn()); expect(mockSocket.send).not.toHaveBeenCalled(); }); }); describe('sendSessionCommand', () => { - it('creates a CommandContainer and calls sendCommand', () => { - const service = new ProtobufService(mockWebClient); + it('stores callback and increments cmdId', () => { + const service = new ProtobufService({ socket: mockSocket } as any); const cb = vi.fn(); - service.sendSessionCommand({ cmdType: 'test' }, cb); - expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith( - expect.objectContaining({ sessionCommand: expect.anything() }) - ); + service.sendSessionCommand({} as any, cb); + expect((service as any).cmdId).toBe(1); + expect((service as any).pendingCommands[1]).toBeTypeOf('function'); }); it('invokes callback with raw response when the pending command is triggered', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const cb = vi.fn(); - service.sendSessionCommand({ cmdType: 'test' }, cb); + service.sendSessionCommand({} as any, cb); const storedCb = (service as any).pendingCommands[1]; storedCb({ responseData: true }); @@ -109,8 +110,8 @@ describe('ProtobufService', () => { }); it('does not throw when no callback is provided and pending command is triggered', () => { - const service = new ProtobufService(mockWebClient); - service.sendSessionCommand({ cmdType: 'test' }); + const service = new ProtobufService({ socket: mockSocket } as any); + service.sendSessionCommand({} as any); const storedCb = (service as any).pendingCommands[1]; expect(() => storedCb({ responseData: true })).not.toThrow(); @@ -118,18 +119,16 @@ describe('ProtobufService', () => { }); describe('sendRoomCommand', () => { - it('creates a CommandContainer with roomId and calls sendCommand', () => { - const service = new ProtobufService(mockWebClient); - service.sendRoomCommand(42, { roomCmdType: 'test' }, vi.fn()); - expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith( - expect.objectContaining({ roomId: 42 }) - ); + it('stores callback and increments cmdId', () => { + const service = new ProtobufService({ socket: mockSocket } as any); + service.sendRoomCommand(42, {} as any, vi.fn()); + expect((service as any).cmdId).toBe(1); }); it('invokes callback with raw response when the pending command is triggered', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const cb = vi.fn(); - service.sendRoomCommand(42, { roomCmdType: 'test' }, cb); + service.sendRoomCommand(42, {} as any, cb); const storedCb = (service as any).pendingCommands[1]; storedCb({ responseData: true }); @@ -138,8 +137,8 @@ describe('ProtobufService', () => { }); it('does not throw when no callback is provided and pending command is triggered', () => { - const service = new ProtobufService(mockWebClient); - service.sendRoomCommand(42, { roomCmdType: 'test' }); + const service = new ProtobufService({ socket: mockSocket } as any); + service.sendRoomCommand(42, {} as any); const storedCb = (service as any).pendingCommands[1]; expect(() => storedCb({ responseData: true })).not.toThrow(); @@ -147,18 +146,16 @@ describe('ProtobufService', () => { }); describe('sendGameCommand', () => { - it('creates a CommandContainer with gameId and gameCommand', () => { - const service = new ProtobufService(mockWebClient); - service.sendGameCommand(7, { gameCmdType: 'test' }, vi.fn()); - expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith( - expect.objectContaining({ gameId: 7, gameCommand: expect.anything() }) - ); + it('stores callback and increments cmdId', () => { + const service = new ProtobufService({ socket: mockSocket } as any); + service.sendGameCommand(7, {} as any, vi.fn()); + expect((service as any).cmdId).toBe(1); }); it('invokes callback with raw response when the pending command is triggered', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const cb = vi.fn(); - service.sendGameCommand(7, { gameCmdType: 'test' }, cb); + service.sendGameCommand(7, {} as any, cb); const storedCb = (service as any).pendingCommands[1]; storedCb({ responseData: true }); @@ -167,8 +164,8 @@ describe('ProtobufService', () => { }); it('does not throw when no callback is provided and pending command is triggered', () => { - const service = new ProtobufService(mockWebClient); - service.sendGameCommand(7, { gameCmdType: 'test' }); + const service = new ProtobufService({ socket: mockSocket } as any); + service.sendGameCommand(7, {} as any); const storedCb = (service as any).pendingCommands[1]; expect(() => storedCb({ responseData: true })).not.toThrow(); @@ -176,18 +173,16 @@ describe('ProtobufService', () => { }); describe('sendModeratorCommand', () => { - it('creates a CommandContainer with moderatorCommand', () => { - const service = new ProtobufService(mockWebClient); - service.sendModeratorCommand({ modCmdType: 'test' }, vi.fn()); - expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith( - expect.objectContaining({ moderatorCommand: expect.anything() }) - ); + it('stores callback and increments cmdId', () => { + const service = new ProtobufService({ socket: mockSocket } as any); + service.sendModeratorCommand({} as any, vi.fn()); + expect((service as any).cmdId).toBe(1); }); it('invokes callback with raw response when the pending command is triggered', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const cb = vi.fn(); - service.sendModeratorCommand({ modCmdType: 'test' }, cb); + service.sendModeratorCommand({} as any, cb); const storedCb = (service as any).pendingCommands[1]; storedCb({ responseData: true }); @@ -196,8 +191,8 @@ describe('ProtobufService', () => { }); it('does not throw when no callback is provided and pending command is triggered', () => { - const service = new ProtobufService(mockWebClient); - service.sendModeratorCommand({ modCmdType: 'test' }); + const service = new ProtobufService({ socket: mockSocket } as any); + service.sendModeratorCommand({} as any); const storedCb = (service as any).pendingCommands[1]; expect(() => storedCb({ responseData: true })).not.toThrow(); @@ -205,18 +200,16 @@ describe('ProtobufService', () => { }); describe('sendAdminCommand', () => { - it('creates a CommandContainer with adminCommand', () => { - const service = new ProtobufService(mockWebClient); - service.sendAdminCommand({ adminCmdType: 'test' }, vi.fn()); - expect(ProtoController.root.CommandContainer.create).toHaveBeenCalledWith( - expect.objectContaining({ adminCommand: expect.anything() }) - ); + it('stores callback and increments cmdId', () => { + const service = new ProtobufService({ socket: mockSocket } as any); + service.sendAdminCommand({} as any, vi.fn()); + expect((service as any).cmdId).toBe(1); }); it('invokes callback with raw response when the pending command is triggered', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const cb = vi.fn(); - service.sendAdminCommand({ adminCmdType: 'test' }, cb); + service.sendAdminCommand({} as any, cb); const storedCb = (service as any).pendingCommands[1]; storedCb({ responseData: true }); @@ -225,8 +218,8 @@ describe('ProtobufService', () => { }); it('does not throw when no callback is provided and pending command is triggered', () => { - const service = new ProtobufService(mockWebClient); - service.sendAdminCommand({ adminCmdType: 'test' }); + const service = new ProtobufService({ socket: mockSocket } as any); + service.sendAdminCommand({} as any); const storedCb = (service as any).pendingCommands[1]; expect(() => storedCb({ responseData: true })).not.toThrow(); @@ -235,7 +228,7 @@ describe('ProtobufService', () => { describe('sendKeepAliveCommand', () => { it('delegates to SessionCommands.ping', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const pingReceived = vi.fn(); service.sendKeepAliveCommand(pingReceived); expect(sessionPing).toHaveBeenCalledWith(pingReceived); @@ -244,96 +237,83 @@ describe('ProtobufService', () => { describe('handleMessageEvent', () => { it('routes RESPONSE message to processServerResponse', () => { - const service = new ProtobufService(mockWebClient); - const cb = vi.fn(); - // store a callback for cmdId 1 - (service as any).cmdId = 1; - (service as any).pendingCommands[1] = cb; - - const response = { cmdId: 1 }; - ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({ - messageType: ProtoController.root.ServerMessage.MessageType.RESPONSE, - response, - }); - - service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); - expect(cb).toHaveBeenCalledWith(response); - expect((service as any).pendingCommands[1]).toBeUndefined(); - }); - - it('resolves pending command when response cmdId is a protobufjs Long object', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const cb = vi.fn(); (service as any).cmdId = 1; (service as any).pendingCommands[1] = cb; - // Simulate protobufjs decoding cmdId as a Long object (low=1, high=0) - const longCmdId = { low: 1, high: 0, unsigned: false, toString: () => '1' }; - const response = { cmdId: longCmdId }; - ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({ - messageType: ProtoController.root.ServerMessage.MessageType.RESPONSE, - response, - }); + vi.mocked(fromBinary).mockReturnValue({ + messageType: ServerMessage_MessageType.RESPONSE, + response: { cmdId: BigInt(1) }, + } as any); service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); - expect(cb).toHaveBeenCalledWith(response); + expect(cb).toHaveBeenCalledWith({ cmdId: BigInt(1) }); expect((service as any).pendingCommands[1]).toBeUndefined(); }); it('routes ROOM_EVENT message', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const processRoomEvent = vi.spyOn(service as any, 'processRoomEvent'); - ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({ - messageType: ProtoController.root.ServerMessage.MessageType.ROOM_EVENT, - roomEvent: { '.Event_Room.ext': {} }, - }); + + vi.mocked(fromBinary).mockReturnValue({ + messageType: ServerMessage_MessageType.ROOM_EVENT, + roomEvent: {}, + } as any); + service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); expect(processRoomEvent).toHaveBeenCalled(); }); it('routes SESSION_EVENT message', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const processSessionEvent = vi.spyOn(service as any, 'processSessionEvent'); - ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({ - messageType: ProtoController.root.ServerMessage.MessageType.SESSION_EVENT, - sessionEvent: { '.Event_Session.ext': {} }, - }); + + vi.mocked(fromBinary).mockReturnValue({ + messageType: ServerMessage_MessageType.SESSION_EVENT, + sessionEvent: {}, + } as any); + service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); expect(processSessionEvent).toHaveBeenCalled(); }); it('routes GAME_EVENT_CONTAINER message', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const processGameEvent = vi.spyOn(service as any, 'processGameEvent'); - ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({ - messageType: ProtoController.root.ServerMessage.MessageType.GAME_EVENT_CONTAINER, - gameEvent: { '.Event_Game.ext': {} }, - }); + + vi.mocked(fromBinary).mockReturnValue({ + messageType: ServerMessage_MessageType.GAME_EVENT_CONTAINER, + gameEventContainer: {}, + } as any); + service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); expect(processGameEvent).toHaveBeenCalled(); }); it('logs unknown message types (default case)', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue({ - messageType: 'UNKNOWN_TYPE', - }); + + vi.mocked(fromBinary).mockReturnValue({ + messageType: 999, + } as any); + service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); it('does nothing when decoded message is null', () => { - const service = new ProtobufService(mockWebClient); - ProtoController.root.ServerMessage.decode = vi.fn().mockReturnValue(null); + const service = new ProtobufService({ socket: mockSocket } as any); + vi.mocked(fromBinary).mockReturnValue(null as any); expect(() => service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent)).not.toThrow(); }); it('catches and logs decode errors', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - ProtoController.root.ServerMessage.decode = vi.fn().mockImplementation(() => { + vi.mocked(fromBinary).mockImplementation(() => { throw new Error('decode error'); }); expect(() => service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent)).not.toThrow(); @@ -344,50 +324,60 @@ describe('ProtobufService', () => { describe('processGameEvent', () => { it('returns early when container has no eventList', () => { - const service = new ProtobufService(mockWebClient); - const gameEventHandler = (GameEvents as any)['.Event_Game.ext'] as vi.Mock; + const service = new ProtobufService({ socket: mockSocket } as any); + vi.mocked(hasExtension).mockReturnValue(false); (service as any).processGameEvent(null, {}); - expect(gameEventHandler).not.toHaveBeenCalled(); + expect(hasExtension).not.toHaveBeenCalled(); }); - it('dispatches to a GameEvents handler when event key matches', () => { - const service = new ProtobufService(mockWebClient); - const gameEventHandler = (GameEvents as any)['.Event_Game.ext'] as vi.Mock; + it('dispatches to a GameEvents handler when hasExtension returns true', () => { + const service = new ProtobufService({ socket: mockSocket } as any); + const handler = vi.fn(); + const mockExt = {}; const payload = { someData: 1 }; + + // Temporarily override GameEvents for this test + (GameEvents as any).push([mockExt, handler]); + vi.mocked(hasExtension).mockReturnValue(true); + vi.mocked(getExtension).mockReturnValue(payload); + (service as any).processGameEvent({ gameId: 42, - context: null, - secondsElapsed: 10, - forcedByJudge: 0, - eventList: [{ '.Event_Game.ext': payload, playerId: 5 }], + eventList: [{ playerId: 5 }], }, {}); - expect(gameEventHandler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: 42, playerId: 5 })); - }); + expect(handler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: 42, playerId: 5 })); + (GameEvents as any).pop(); + }); }); describe('processEvent', () => { it('calls matching event handler with payload and raw', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const handler = vi.fn(); - const events = { '.Event_Test.ext': handler }; + const mockExt = {}; + const registry = [[mockExt, handler]] as any; const payload = { someData: 1 }; - const response = { '.Event_Test.ext': payload }; const raw = { extra: true }; - (service as any).processEvent(response, events, raw); + vi.mocked(hasExtension).mockReturnValue(true); + vi.mocked(getExtension).mockReturnValue(payload); + + (service as any).processEvent({}, registry, raw); expect(handler).toHaveBeenCalledWith(payload, raw); }); it('stops after first matching event', () => { - const service = new ProtobufService(mockWebClient); + const service = new ProtobufService({ socket: mockSocket } as any); const handler1 = vi.fn(); const handler2 = vi.fn(); - const events = { '.Event_A.ext': handler1, '.Event_B.ext': handler2 }; - const response = { '.Event_A.ext': { x: 1 } }; + const registry = [[{}, handler1], [{}, handler2]] as any; - (service as any).processEvent(response, events, {}); + vi.mocked(hasExtension).mockReturnValueOnce(true).mockReturnValueOnce(false); + vi.mocked(getExtension).mockReturnValue({ x: 1 }); + + (service as any).processEvent({}, registry, {}); expect(handler1).toHaveBeenCalled(); expect(handler2).not.toHaveBeenCalled(); diff --git a/webclient/src/websocket/services/ProtobufService.ts b/webclient/src/websocket/services/ProtobufService.ts index 1be84d81d..1874cf50d 100644 --- a/webclient/src/websocket/services/ProtobufService.ts +++ b/webclient/src/websocket/services/ProtobufService.ts @@ -1,12 +1,25 @@ +import { create, fromBinary, hasExtension, getExtension, toBinary } from '@bufbuild/protobuf'; +import type { GenExtension, Message } from '@bufbuild/protobuf'; + +import type { Response } from 'generated/proto/response_pb'; +import type { RoomEvent } from 'generated/proto/room_event_pb'; +import type { SessionEvent } from 'generated/proto/session_event_pb'; +import type { GameEventContainer } from 'generated/proto/game_event_container_pb'; + import { GameEvents, RoomEvents, SessionEvents } from '../events'; import { WebClient } from '../WebClient'; import { SessionCommands } from 'websocket'; -import { ProtoController } from './ProtoController'; import { GameEventMeta } from 'types'; -export interface ProtobufEvents { - [event: string]: Function; -} +import { CommandContainerSchema, type CommandContainer } from 'generated/proto/commands_pb'; +import { ServerMessageSchema, ServerMessage_MessageType, type ServerMessage } from 'generated/proto/server_message_pb'; +import type { SessionCommand } from 'generated/proto/session_commands_pb'; +import type { GameCommand } from 'generated/proto/game_commands_pb'; +import type { RoomCommand } from 'generated/proto/room_commands_pb'; +import type { ModeratorCommand } from 'generated/proto/moderator_commands_pb'; +import type { AdminCommand } from 'generated/proto/admin_commands_pb'; + +export type ExtensionRegistry = Array<[GenExtension, (...args: unknown[]) => void]>; export class ProtobufService { private cmdId = 0; @@ -16,7 +29,6 @@ export class ProtobufService { constructor(webClient: WebClient) { this.webClient = webClient; - ProtoController.load(); } public resetCommands() { @@ -24,56 +36,51 @@ export class ProtobufService { this.pendingCommands = {}; } - public sendGameCommand(gameId: number, gameCmd: any, callback?: Function) { - const cmd = ProtoController.root.CommandContainer.create({ + public sendGameCommand(gameId: number, gameCmd: GameCommand, callback?: Function) { + const cmd = create(CommandContainerSchema, { gameId, gameCommand: [gameCmd], }); - - this.sendCommand(cmd, (raw: any) => callback && callback(raw)); + this.sendCommand(cmd, (raw: Response) => callback && callback(raw)); } - public sendRoomCommand(roomId: number, roomCmd: any, callback?: Function) { - const cmd = ProtoController.root.CommandContainer.create({ - 'roomId': roomId, - 'roomCommand': [roomCmd] + public sendRoomCommand(roomId: number, roomCmd: RoomCommand, callback?: Function) { + const cmd = create(CommandContainerSchema, { + roomId, + roomCommand: [roomCmd], }); - this.sendCommand(cmd, raw => callback && callback(raw)); } - public sendSessionCommand(sesCmd: any, callback?: Function) { - const cmd = ProtoController.root.CommandContainer.create({ - 'sessionCommand': [sesCmd] + public sendSessionCommand(sesCmd: SessionCommand, callback?: Function) { + const cmd = create(CommandContainerSchema, { + sessionCommand: [sesCmd], }); - this.sendCommand(cmd, (raw) => callback && callback(raw)); } - public sendModeratorCommand(modCmd: any, callback?: Function) { - const cmd = ProtoController.root.CommandContainer.create({ - 'moderatorCommand': [modCmd] + public sendModeratorCommand(modCmd: ModeratorCommand, callback?: Function) { + const cmd = create(CommandContainerSchema, { + moderatorCommand: [modCmd], }); - this.sendCommand(cmd, (raw) => callback && callback(raw)); } - public sendAdminCommand(adminCmd: any, callback?: Function) { - const cmd = ProtoController.root.CommandContainer.create({ - 'adminCommand': [adminCmd] + public sendAdminCommand(adminCmd: AdminCommand, callback?: Function) { + const cmd = create(CommandContainerSchema, { + adminCommand: [adminCmd], }); - this.sendCommand(cmd, (raw) => callback && callback(raw)); } - public sendCommand(cmd: any, callback: Function) { + public sendCommand(cmd: CommandContainer, callback: Function) { this.cmdId++; - cmd['cmdId'] = this.cmdId; + cmd.cmdId = BigInt(this.cmdId); this.pendingCommands[this.cmdId] = callback; if (this.webClient.socket.checkReadyState(WebSocket.OPEN)) { - this.webClient.socket.send(ProtoController.root.CommandContainer.encode(cmd).finish()); + this.webClient.socket.send(toBinary(CommandContainerSchema, cmd)); } } @@ -84,21 +91,21 @@ export class ProtobufService { public handleMessageEvent({ data }: MessageEvent): void { try { const uint8msg = new Uint8Array(data); - const msg = ProtoController.root.ServerMessage.decode(uint8msg); + const msg: ServerMessage = fromBinary(ServerMessageSchema, uint8msg); if (msg) { switch (msg.messageType) { - case ProtoController.root.ServerMessage.MessageType.RESPONSE: + case ServerMessage_MessageType.RESPONSE: this.processServerResponse(msg.response); break; - case ProtoController.root.ServerMessage.MessageType.ROOM_EVENT: - this.processRoomEvent(msg.roomEvent, msg); + case ServerMessage_MessageType.ROOM_EVENT: + this.processRoomEvent(msg.roomEvent); break; - case ProtoController.root.ServerMessage.MessageType.SESSION_EVENT: - this.processSessionEvent(msg.sessionEvent, msg); + case ServerMessage_MessageType.SESSION_EVENT: + this.processSessionEvent(msg.sessionEvent); break; - case ProtoController.root.ServerMessage.MessageType.GAME_EVENT_CONTAINER: - this.processGameEvent(msg.gameEventContainer, msg); + case ServerMessage_MessageType.GAME_EVENT_CONTAINER: + this.processGameEvent(msg.gameEventContainer); break; default: console.log(msg); @@ -110,8 +117,11 @@ export class ProtobufService { } } - private processServerResponse(response: any) { - const { cmdId } = response; + private processServerResponse(response: Response | undefined) { + if (!response) { + return; + } + const cmdId = Number(response.cmdId); if (this.pendingCommands[cmdId]) { this.pendingCommands[cmdId](response); @@ -119,15 +129,21 @@ export class ProtobufService { } } - private processRoomEvent(response: any, raw: any) { - this.processEvent(response, RoomEvents, raw); + private processRoomEvent(event: RoomEvent | undefined) { + if (!event) { + return; + } + this.processEvent(event, RoomEvents, event); } - private processSessionEvent(response: any, raw: any) { - this.processEvent(response, SessionEvents, raw); + private processSessionEvent(event: SessionEvent | undefined) { + if (!event) { + return; + } + this.processEvent(event, SessionEvents); } - private processGameEvent(container: any, raw: any): void { + private processGameEvent(container: GameEventContainer | undefined): void { if (!container?.eventList?.length) { return; } @@ -143,24 +159,22 @@ export class ProtobufService { forcedByJudge: forcedByJudge ?? 0, }; - for (const key of Object.keys(GameEvents)) { - const payload = event[key]; - if (payload !== undefined && payload !== null) { - (GameEvents[key] as Function)(payload, meta); + for (const [ext, handler] of GameEvents) { + if (hasExtension(event, ext)) { + (handler as Function)(getExtension(event, ext), meta); break; } } } } - private processEvent(response: any, events: ProtobufEvents, raw: any) { - for (const event in events) { - const payload = response[event]; - - if (payload !== undefined && payload !== null) { - events[event](payload, raw); + private processEvent(response: Message, registry: ExtensionRegistry, raw?: Message) { + for (const [ext, handler] of registry) { + if (hasExtension(response, ext)) { + (handler as Function)(getExtension(response, ext), raw); return; } } } } + diff --git a/webclient/src/websocket/utils/passwordHasher.spec.ts b/webclient/src/websocket/utils/passwordHasher.spec.ts index a8a4148af..7ab16d128 100644 --- a/webclient/src/websocket/utils/passwordHasher.spec.ts +++ b/webclient/src/websocket/utils/passwordHasher.spec.ts @@ -1,16 +1,9 @@ -import { makeMockProtoRoot } from '../__mocks__/helpers'; - -vi.mock('../services/ProtoController', () => ({ - ProtoController: { root: null }, +vi.mock('generated/proto/event_server_identification_pb', () => ({ + Event_ServerIdentification_ServerOptions: { SupportsPasswordHash: 2 }, })); -import { ProtoController } from '../services/ProtoController'; import { hashPassword, generateSalt, passwordSaltSupported } from './passwordHasher'; -beforeEach(() => { - ProtoController.root = makeMockProtoRoot(); -}); - describe('hashPassword', () => { it('returns a string starting with the salt', () => { const result = hashPassword('mysalt', 'mypassword'); diff --git a/webclient/src/websocket/utils/passwordHasher.ts b/webclient/src/websocket/utils/passwordHasher.ts index 164a91823..3f726f10f 100644 --- a/webclient/src/websocket/utils/passwordHasher.ts +++ b/webclient/src/websocket/utils/passwordHasher.ts @@ -1,6 +1,6 @@ import sha512 from 'crypto-js/sha512'; import Base64 from 'crypto-js/enc-base64'; -import { ProtoController } from '../services/ProtoController'; +import { Event_ServerIdentification_ServerOptions } from 'generated/proto/event_server_identification_pb'; const HASH_ROUNDS = 1_000; const SALT_LENGTH = 16; @@ -28,5 +28,5 @@ export const generateSalt = (): string => { export const passwordSaltSupported = (serverOptions: number): number => { // Intentional use of Bitwise operator b/c of how Servatrice Enums work - return serverOptions & ProtoController.root.Event_ServerIdentification.ServerOptions.SupportsPasswordHash; + return serverOptions & Event_ServerIdentification_ServerOptions.SupportsPasswordHash; } diff --git a/webclient/tsconfig.json b/webclient/tsconfig.json index 1287c0521..3ce50dc96 100644 --- a/webclient/tsconfig.json +++ b/webclient/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "baseUrl": "src", - "target": "es6", + "target": "es2020", "lib": [ "dom", "dom.iterable",