Merge remote-tracking branch 'misskey-dev/develop' into io

This commit is contained in:
まっちゃとーにゅ 2024-01-23 15:39:41 +09:00
commit 05b1b32f8e
No known key found for this signature in database
GPG key ID: 143DE582A97FE052
19 changed files with 269 additions and 53 deletions

View file

@ -383,9 +383,11 @@ hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptcha 활성화"
hcaptchaSiteKey: "사이트 키"
hcaptchaSecretKey: "시크릿 키"
mcaptcha: "mCaptcha"
enableMcaptcha: "mCaptcha 활성화"
mcaptchaSiteKey: "사이트 키"
mcaptchaSecretKey: "시크릿 키"
mcaptchaInstanceUrl: "mCaptcha 인스턴스 URL"
recaptcha: "reCAPTCHA"
enableRecaptcha: "reCAPTCHA 활성화"
recaptchaSiteKey: "사이트 키"
@ -633,6 +635,7 @@ medium: "보통"
small: "작게"
generateAccessToken: "액세스 토큰 생성"
permission: "권한"
adminPermission: "관리자 권한"
enableAll: "전체 선택"
disableAll: "전체 해제"
tokenRequested: "계정 접근 허용"
@ -676,6 +679,7 @@ useGlobalSettingDesc: "활성화하면 계정의 알림 설정이 적용됩니
other: "기타"
regenerateLoginToken: "로그인 토큰을 재생성"
regenerateLoginTokenDescription: "로그인할 때 사용되는 내부 토큰을 재생성합니다. 일반적으로 이 작업을 실행할 필요는 없습니다. 이 기능을 사용하면 이 계정으로 로그인한 모든 기기에서 로그아웃됩니다."
theKeywordWhenSearchingForCustomEmoji: "맞춤 이모티콘을 검색할 때 키워드가 됩니다."
setMultipleBySeparatingWithSpace: "공백으로 구분하여 여러 개 설정할 수 있습니다."
fileIdOrUrl: "파일 ID 또는 URL"
behavior: "동작"
@ -1058,6 +1062,8 @@ limitWidthOfReaction: "리액션의 최대 폭을 제한하고 작게 표시하
noteIdOrUrl: "노트 ID 및 URL"
video: "동영상"
videos: "동영상"
audio: "소리"
audioFiles: "소리"
dataSaver: "데이터 절약 모드"
accountMigration: "계정 이동"
accountMoved: "이 사용자는 다음 계정으로 이사했습니다:"
@ -1190,10 +1196,25 @@ seasonalScreenEffect: "계절에 따른 효과 보이기"
decorate: "장식하기"
addMfmFunction: "장식 추가하기"
enableQuickAddMfmFunction: "상급자용 MFM 선택기 표시하기"
bubbleGame: "버블 게임"
sfx: "효과음"
soundWillBePlayed: "소리가 재생됩니다"
showReplay: "리플레이 보기"
replay: "리플레이"
replaying: "리플레이 중"
ranking: "랭킹"
lastNDays: "최근 {n}일"
backToTitle: "타이틀로 가기"
hemisphere: "거주 지역"
withSensitive: "민감한 파일이 포함된 노트 보기"
userSaysSomethingSensitive: "{name}의 민감한 파일이 포함된 게시물"
enableHorizontalSwipe: "스와이프하여 탭 전환"
_bubbleGame:
howToPlay: "설명"
_howToPlay:
section1: "위치를 조정하여 상자에 물건을 떨어뜨립니다."
section2: "같은 종류의 물건이 붙으면 다른 물건으로 바뀌면서 점수를 얻게 됩니다."
section3: "상자에서 물건이 넘치면 게임 오버입니다. 상자에서 물건이 넘치지 않도록 하면서 물건을 융합하여 높은 점수를 획득하세요!"
abuseReportCategory: "신고 유형"
selectCategory: "카테고리 선택"
reportComplete: "신고 완료"
@ -1582,6 +1603,13 @@ _achievements:
_tutorialCompleted:
title: "Misskey 입문자 과정 수료증"
description: "튜토리얼을 완료했습니다"
_bubbleGameExplodingHead:
title: "🤯"
description: "버블 게임에서 가장 큰 물건을 내놓았다"
_bubbleGameDoubleExplodingHead:
title: "더블 🤯"
description: "버블게임에서 가장 큰 물건 2개를 동시에 내놓았다."
flavor: "이 정도만 도시락통에 🤯 🤯 조금만 더"
_role:
new: "새 역할 생성"
edit: "역할 수정"
@ -2435,6 +2463,48 @@ _dataSaver:
_code:
title: "문자열 강조"
description: "MFM 등으로 문자열 강조 기법을 사용할 때 누르기 전에는 불러오지 않습니다. 문자열 강조에서는 강조할 언어마다 그 정의 파일을 불러와야 하지만 이를 자동으로 불러오지 않으므로 데이터 사용량을 줄일 수 있습니다."
_hemisphere:
N: "북반구"
S: "남반구"
caption: "일부 클라이언트 설정에서 계절을 판단하기 위해 사용합니다."
_reversi:
reversi: "리버시"
gameSettings: "대국 설정"
chooseBoard: "보드 선택"
blackOrWhite: "선공/후공"
blackIs: "{name}님이 흑(선공)"
rules: "규칙"
thisGameIsStartedSoon: "대국이 곧 시작됩니다"
waitingForOther: "상대방의 준비가 완료되기를 기다리고 있습니다."
waitingForMe: "당신의 준비가 완료되기를 기다리고 있습니다."
waitingBoth: "준비하세요"
ready: "준비 완료"
cancelReady: "준비 다시 시작"
opponentTurn: "상대의 차례입니다"
myTurn: "당신의 차례입니다"
turnOf: "{name}의 차례입니다"
pastTurnOf: "{name}의 차례"
surrender: "기권"
surrendered: "기권에 의해"
timeout: "시간 초과"
drawn: "무승부"
won: "{name}의 승리"
black: "흑"
white: "백"
total: "합계"
turnCount: "{count}턴 째"
myGames: "내 대국"
allGames: "모두의 대국"
ended: "종료"
playing: "대국 중"
isLlotheo: "돌이 적은 사람이 승리 (로세오)"
loopedMap: "루프 지도"
canPutEverywhere: "어디에도 둘 수 있는 모드"
timeLimitForEachTurn: "1턴의 시간 제한"
freeMatch: "프리매치"
lookingForPlayer: "상대를 찾고 있습니다"
gameCanceled: "대국이 취소되었습니다"
_offlineScreen:
title: "오프라인 - 서버에 접속할 수 없습니다"
header: "서버에 접속할 수 없습니다"

View file

@ -2473,7 +2473,7 @@ _reversi:
turnCount: "{count} 回合"
myGames: "我的對弈"
allGames: "所有對弈"
ended: ""
ended: "已結束"
playing: "正在對弈"
isLlotheo: "子較少的一方為勝(顛倒規則)"
loopedMap: "循環棋盤"

View file

@ -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",

View file

@ -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 type {
@ -254,7 +253,13 @@ export class ReversiService implements 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({
@ -275,12 +280,6 @@ export class ReversiService implements 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) {
@ -405,7 +404,7 @@ export class ReversiService implements OnModuleInit {
const serializeLogs = Reversi.Serializer.serializeLogs(logs);
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
const crc32 = engine.calcCrc32().toString();
const updatedGame = {
...game,
@ -538,7 +537,7 @@ export class ReversiService implements OnModuleInit {
if (game == null) throw new Error('game not found');
if (crc32.toString() !== game.crc32) {
return await this.reversiGameEntityService.packDetail(game, null);
return game;
} else {
return null;
}

View file

@ -377,6 +377,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';
@ -752,6 +753,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: [
@ -1131,6 +1133,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass
$reversi_invitations,
$reversi_showGame,
$reversi_surrender,
$reversi_verify,
],
exports: [
$admin_meta,
@ -1501,6 +1504,7 @@ const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass
$reversi_invitations,
$reversi_showGame,
$reversi_surrender,
$reversi_verify,
],
})
export class EndpointsModule {}

View file

@ -378,6 +378,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],
@ -751,6 +752,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 {

View file

@ -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, me),
};
} else {
return {
desynced: false,
};
}
});
}
}

View file

@ -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,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 185 KiB

View file

@ -42,7 +42,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",

View file

@ -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);
}
});

View file

@ -1662,6 +1662,8 @@ declare namespace entities {
ReversiShowGameRequest,
ReversiShowGameResponse,
ReversiSurrenderRequest,
ReversiVerifyRequest,
ReversiVerifyResponse,
Error_2 as Error,
UserLite,
UserDetailedNotMeOnly,
@ -2691,6 +2693,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'];

View file

@ -4127,5 +4127,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>>;
}
}

View file

@ -561,6 +561,8 @@ import type {
ReversiShowGameRequest,
ReversiShowGameResponse,
ReversiSurrenderRequest,
ReversiVerifyRequest,
ReversiVerifyResponse,
} from './entities.js';
export type Endpoints = {
@ -935,4 +937,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 };
}

View file

@ -563,3 +563,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'];

View file

@ -3570,6 +3570,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>;
@ -26483,5 +26492,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'];
};
};
};
};
};

View file

@ -14,8 +14,8 @@
},
"scripts": {
"build": "node ./build.js",
"build:tsc": "npm run ts",
"tsc": "npm run ts-esm && npm run ts-dts",
"build:tsc": "npm run tsc",
"tsc": "npm run tsc-esm && npm run tsc-dts",
"tsc-esm": "tsc --outDir built/esm",
"tsc-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build:tsc\"",
@ -33,8 +33,9 @@
"typescript": "5.3.3"
},
"dependencies": {
"crc-32": "1.2.2",
"esbuild": "0.19.11",
"glob": "^10.3.10"
"glob": "10.3.10"
},
"files": [
"built"

View file

@ -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;
}

View file

@ -186,9 +186,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
@ -743,9 +740,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
@ -1178,11 +1172,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':