diff --git a/packages/backend/package.json b/packages/backend/package.json index 412c7ab8e4..9551991b34 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -107,7 +107,6 @@ "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", - "crc-32": "^1.2.2", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", "fastify": "4.25.2", diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 0d5f989c11..66296f1ed4 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -5,7 +5,6 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import CRC32 from 'crc-32'; import { ModuleRef } from '@nestjs/core'; import * as Reversi from 'misskey-reversi'; import { IsNull } from 'typeorm'; @@ -255,7 +254,13 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { bw = parseInt(game.bw, 10); } - const crc32 = CRC32.str(JSON.stringify(game.logs)).toString(); + const engine = new Reversi.Game(game.map, { + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + }); + + const crc32 = engine.calcCrc32().toString(); const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update() .set({ @@ -276,12 +281,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { this.cacheGame(updatedGame); //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 - const engine = new Reversi.Game(updatedGame.map, { - isLlotheo: updatedGame.isLlotheo, - canPutEverywhere: updatedGame.canPutEverywhere, - loopedBoard: updatedGame.loopedBoard, - }); - if (engine.isEnded) { let winnerId; if (engine.winner === true) { @@ -406,7 +405,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const serializeLogs = Reversi.Serializer.serializeLogs(logs); - const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString(); + const crc32 = engine.calcCrc32().toString(); const updatedGame = { ...game, @@ -536,7 +535,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { if (game == null) throw new Error('game not found'); if (crc32.toString() !== game.crc32) { - return await this.reversiGameEntityService.packDetail(game); + return game; } else { return null; } diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index df69ce2385..e74441834e 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -372,6 +372,7 @@ import * as ep___reversi_match from './endpoints/reversi/match.js'; import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; +import * as ep___reversi_verify from './endpoints/reversi/verify.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; @@ -742,6 +743,7 @@ const $reversi_match: Provider = { provide: 'ep:reversi/match', useClass: ep___r const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useClass: ep___reversi_invitations.default }; const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default }; const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default }; +const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep___reversi_verify.default }; @Module({ imports: [ @@ -1116,6 +1118,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass $reversi_invitations, $reversi_showGame, $reversi_surrender, + $reversi_verify, ], exports: [ $admin_meta, @@ -1481,6 +1484,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass $reversi_invitations, $reversi_showGame, $reversi_surrender, + $reversi_verify, ], }) export class EndpointsModule {} diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 0f2c8cb754..4a88216d06 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -373,6 +373,7 @@ import * as ep___reversi_match from './endpoints/reversi/match.js'; import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; +import * as ep___reversi_verify from './endpoints/reversi/verify.js'; const eps = [ ['admin/meta', ep___admin_meta], @@ -741,6 +742,7 @@ const eps = [ ['reversi/invitations', ep___reversi_invitations], ['reversi/show-game', ep___reversi_showGame], ['reversi/surrender', ep___reversi_surrender], + ['reversi/verify', ep___reversi_verify], ]; interface IEndpointMetaBase { diff --git a/packages/backend/src/server/api/endpoints/reversi/verify.ts b/packages/backend/src/server/api/endpoints/reversi/verify.ts new file mode 100644 index 0000000000..5f5af6ce67 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/verify.ts @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ReversiService } from '@/core/ReversiService.js'; +import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + errors: { + noSuchGame: { + message: 'No such game.', + code: 'NO_SUCH_GAME', + id: '8fb05624-b525-43dd-90f7-511852bdfeee', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + properties: { + desynced: { type: 'boolean' }, + game: { + type: 'object', + optional: true, nullable: true, + ref: 'ReversiGameDetailed', + }, + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + gameId: { type: 'string', format: 'misskey:id' }, + crc32: { type: 'string' }, + }, + required: ['gameId', 'crc32'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + private reversiService: ReversiService, + private reversiGameEntityService: ReversiGameEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const game = await this.reversiService.checkCrc(ps.gameId, ps.crc32); + if (game) { + return { + desynced: true, + game: await this.reversiGameEntityService.packDetail(game), + }; + } else { + return { + desynced: false, + }; + } + }); + } +} diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts index 820c80006b..fb24a29b75 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { MiReversiGame, ReversiGamesRepository } from '@/models/_.js'; +import type { MiReversiGame } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { ReversiService } from '@/core/ReversiService.js'; @@ -19,7 +19,6 @@ class ReversiGameChannel extends Channel { constructor( private reversiService: ReversiService, - private reversiGamesRepository: ReversiGamesRepository, private reversiGameEntityService: ReversiGameEntityService, id: string, @@ -42,7 +41,6 @@ class ReversiGameChannel extends Channel { case 'updateSettings': this.updateSettings(body.key, body.value); break; case 'cancel': this.cancelGame(); break; case 'putStone': this.putStone(body.pos, body.id); break; - case 'resync': this.resync(body.crc32); break; case 'claimTimeIsUp': this.claimTimeIsUp(); break; } } @@ -75,14 +73,6 @@ class ReversiGameChannel extends Channel { this.reversiService.putStoneToGame(this.gameId!, this.user, pos, id); } - @bindThis - private async resync(crc32: string | number) { - const game = await this.reversiService.checkCrc(this.gameId!, crc32); - if (game) { - this.send('resynced', game); - } - } - @bindThis private async claimTimeIsUp() { if (this.user == null) return; @@ -104,9 +94,6 @@ export class ReversiGameChannelService implements MiChannelService<false> { public readonly kind = ReversiGameChannel.kind; constructor( - @Inject(DI.reversiGamesRepository) - private reversiGamesRepository: ReversiGamesRepository, - private reversiService: ReversiService, private reversiGameEntityService: ReversiGameEntityService, ) { @@ -116,7 +103,6 @@ export class ReversiGameChannelService implements MiChannelService<false> { public create(id: string, connection: Channel['connection']): ReversiGameChannel { return new ReversiGameChannel( this.reversiService, - this.reversiGamesRepository, this.reversiGameEntityService, id, connection, diff --git a/packages/frontend/package.json b/packages/frontend/package.json index a93679e359..a926daa17d 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -41,7 +41,6 @@ "chartjs-plugin-zoom": "2.0.1", "chromatic": "10.3.1", "compare-versions": "6.1.0", - "crc-32": "^1.2.2", "cropperjs": "2.0.0-beta.4", "date-fns": "2.30.0", "defu": "^6.1.4", diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index d492296c16..8b59df06f7 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -143,7 +143,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue'; -import * as CRC32 from 'crc-32'; import * as Misskey from 'misskey-js'; import * as Reversi from 'misskey-reversi'; import MkButton from '@/components/MkButton.vue'; @@ -240,11 +239,17 @@ watch(logPos, (v) => { if (game.value.isStarted && !game.value.isEnded) { useInterval(() => { - if (game.value.isEnded || props.connection == null) return; - const crc32 = CRC32.str(JSON.stringify(game.value.logs)).toString(); + if (game.value.isEnded) return; + const crc32 = engine.value.calcCrc32(); if (_DEV_) console.log('crc32', crc32); - props.connection.send('resync', { - crc32: crc32, + misskeyApi('reversi/verify', { + gameId: game.value.id, + crc32: crc32.toString(), + }).then((res) => { + if (res.desynced) { + console.log('resynced'); + restoreGame(res.game!); + } }); }, 10000, { immediate: false, afterMounted: true }); } @@ -392,12 +397,6 @@ function restoreGame(_game) { checkEnd(); } -function onStreamResynced(_game) { - console.log('resynced'); - - restoreGame(_game); -} - async function surrender() { const { canceled } = await os.confirm({ type: 'warning', @@ -450,7 +449,6 @@ function share() { onMounted(() => { if (props.connection != null) { props.connection.on('log', onStreamLog); - props.connection.on('resynced', onStreamResynced); props.connection.on('ended', onStreamEnded); } }); @@ -458,7 +456,6 @@ onMounted(() => { onActivated(() => { if (props.connection != null) { props.connection.on('log', onStreamLog); - props.connection.on('resynced', onStreamResynced); props.connection.on('ended', onStreamEnded); } }); @@ -466,7 +463,6 @@ onActivated(() => { onDeactivated(() => { if (props.connection != null) { props.connection.off('log', onStreamLog); - props.connection.off('resynced', onStreamResynced); props.connection.off('ended', onStreamEnded); } }); @@ -474,7 +470,6 @@ onDeactivated(() => { onUnmounted(() => { if (props.connection != null) { props.connection.off('log', onStreamLog); - props.connection.off('resynced', onStreamResynced); props.connection.off('ended', onStreamEnded); } }); diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 2b95e01533..26f100e452 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1633,6 +1633,8 @@ declare namespace entities { ReversiShowGameRequest, ReversiShowGameResponse, ReversiSurrenderRequest, + ReversiVerifyRequest, + ReversiVerifyResponse, Error_2 as Error, UserLite, UserDetailedNotMeOnly, @@ -2644,6 +2646,12 @@ type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200 // @public (undocumented) type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json']; +// @public (undocumented) +type ReversiVerifyRequest = operations['reversi/verify']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiVerifyResponse = operations['reversi/verify']['responses']['200']['content']['application/json']; + // @public (undocumented) type Role = components['schemas']['Role']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 17e3376be7..fe04658deb 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.2 - * generatedAt: 2024-01-22T07:11:08.412Z + * version: 2024.2.0-beta.3 + * generatedAt: 2024-01-23T01:22:13.177Z */ import type { SwitchCaseResponseType } from '../api.js'; @@ -4073,5 +4073,16 @@ declare module '../api.js' { params: P, credential?: string | null, ): Promise<SwitchCaseResponseType<E, P>>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request<E extends 'reversi/verify', P extends Endpoints[E]['req']>( + endpoint: E, + params: P, + credential?: string | null, + ): Promise<SwitchCaseResponseType<E, P>>; } } diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 97268268fc..0060003031 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.2 - * generatedAt: 2024-01-22T07:11:08.410Z + * version: 2024.2.0-beta.3 + * generatedAt: 2024-01-23T01:22:13.175Z */ import type { @@ -554,6 +554,8 @@ import type { ReversiShowGameRequest, ReversiShowGameResponse, ReversiSurrenderRequest, + ReversiVerifyRequest, + ReversiVerifyResponse, } from './entities.js'; export type Endpoints = { @@ -923,4 +925,5 @@ export type Endpoints = { 'reversi/invitations': { req: EmptyRequest; res: ReversiInvitationsResponse }; 'reversi/show-game': { req: ReversiShowGameRequest; res: ReversiShowGameResponse }; 'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse }; + 'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse }; } diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 8fc9b1db1d..55afcdeb31 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.2 - * generatedAt: 2024-01-22T07:11:08.408Z + * version: 2024.2.0-beta.3 + * generatedAt: 2024-01-23T01:22:13.173Z */ import { operations } from './types.js'; @@ -556,3 +556,5 @@ export type ReversiInvitationsResponse = operations['reversi/invitations']['resp export type ReversiShowGameRequest = operations['reversi/show-game']['requestBody']['content']['application/json']; export type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200']['content']['application/json']; export type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['content']['application/json']; +export type ReversiVerifyRequest = operations['reversi/verify']['requestBody']['content']['application/json']; +export type ReversiVerifyResponse = operations['reversi/verify']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 2402fd53ae..c94dfaa25e 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* - * version: 2024.2.0-beta.2 - * generatedAt: 2024-01-22T07:11:08.408Z + * version: 2024.2.0-beta.3 + * generatedAt: 2024-01-23T01:22:13.172Z */ import { components } from './types.js'; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 3504f6fa7d..761304ef28 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -2,8 +2,8 @@ /* eslint @typescript-eslint/no-explicit-any: 0 */ /* - * version: 2024.2.0-beta.2 - * generatedAt: 2024-01-22T07:11:08.327Z + * version: 2024.2.0-beta.3 + * generatedAt: 2024-01-23T01:22:13.093Z */ /** @@ -3526,6 +3526,15 @@ export type paths = { */ post: operations['reversi/surrender']; }; + '/reversi/verify': { + /** + * reversi/verify + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['reversi/verify']; + }; }; export type webhooks = Record<string, never>; @@ -25984,5 +25993,63 @@ export type operations = { }; }; }; + /** + * reversi/verify + * @description No description provided. + * + * **Credential required**: *No* + */ + 'reversi/verify': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + gameId: string; + crc32: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': { + desynced: boolean; + game?: components['schemas']['ReversiGameDetailed'] | null; + }; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; }; diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json index 47793200ef..bd8d4b498c 100644 --- a/packages/misskey-reversi/package.json +++ b/packages/misskey-reversi/package.json @@ -36,7 +36,8 @@ "built" ], "dependencies": { + "crc-32": "1.2.2", "esbuild": "0.19.11", - "glob": "^10.3.10" + "glob": "10.3.10" } } diff --git a/packages/misskey-reversi/src/game.ts b/packages/misskey-reversi/src/game.ts index f29b001447..caad17f9f2 100644 --- a/packages/misskey-reversi/src/game.ts +++ b/packages/misskey-reversi/src/game.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import CRC32 from 'crc-32'; + /** * true ... 黒 * false ... 白 @@ -204,6 +206,13 @@ export class Game { return ([] as number[]).concat(...diffVectors.map(effectsInLine)); } + public calcCrc32(): number { + return CRC32.str(JSON.stringify({ + board: this.board, + turn: this.turn, + })); + } + public get isEnded(): boolean { return this.turn === null; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05c245a100..c710f71a85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,9 +185,6 @@ importers: content-disposition: specifier: 0.5.4 version: 0.5.4 - crc-32: - specifier: ^1.2.2 - version: 1.2.2 date-fns: specifier: 2.30.0 version: 2.30.0 @@ -742,9 +739,6 @@ importers: compare-versions: specifier: 6.1.0 version: 6.1.0 - crc-32: - specifier: ^1.2.2 - version: 1.2.2 cropperjs: specifier: 2.0.0-beta.4 version: 2.0.0-beta.4 @@ -1177,11 +1171,14 @@ importers: packages/misskey-reversi: dependencies: + crc-32: + specifier: 1.2.2 + version: 1.2.2 esbuild: specifier: 0.19.11 version: 0.19.11 glob: - specifier: ^10.3.10 + specifier: 10.3.10 version: 10.3.10 devDependencies: '@misskey-dev/eslint-plugin':