From d429f810a9ea3fda9efed2ff51483d25a288ecc9 Mon Sep 17 00:00:00 2001 From: Ebise Lutica <7106976+EbiseLutica@users.noreply.github.com> Date: Thu, 13 Apr 2023 00:31:22 +0900 Subject: [PATCH 0001/1120] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41353c346b..df2265727d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ ## 13.11.2 +### Note +- 13.11.0または13.11.1から13.11.2以降にアップデートする場合、Redisのカスタム絵文字のキャッシュを削除する必要があります(https://github.com/misskey-dev/misskey/issues/10502#issuecomment-1502790755 参照) + ### General - チャンネルの検索用ページの追加 From 5bcdd6e84931d4fc9a202b334ab8409c8dd65ae0 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 17 Jan 2024 16:16:16 +0900 Subject: [PATCH 0002/1120] wip --- .../migration/1705475608437-reversi.js | 22 + packages/backend/package.json | 1 + packages/backend/src/di-symbols.ts | 2 + .../backend/src/models/RepositoryModule.ts | 20 +- packages/backend/src/models/ReversiGame.ts | 127 ++++ .../backend/src/models/ReversiMatching.ts | 29 + packages/backend/src/models/_.ts | 7 + packages/backend/src/postgres.ts | 4 + .../api/stream/channels/reversi-game.ts | 359 +++++++++ .../src/server/api/stream/channels/reversi.ts | 0 packages/reversi/package.json | 26 + packages/reversi/src/engine.ts | 216 ++++++ packages/reversi/src/maps.ts | 715 ++++++++++++++++++ packages/reversi/tsconfig.json | 33 + pnpm-lock.yaml | 3 + 15 files changed, 1562 insertions(+), 2 deletions(-) create mode 100644 packages/backend/migration/1705475608437-reversi.js create mode 100644 packages/backend/src/models/ReversiGame.ts create mode 100644 packages/backend/src/models/ReversiMatching.ts create mode 100644 packages/backend/src/server/api/stream/channels/reversi-game.ts create mode 100644 packages/backend/src/server/api/stream/channels/reversi.ts create mode 100644 packages/reversi/package.json create mode 100644 packages/reversi/src/engine.ts create mode 100644 packages/reversi/src/maps.ts create mode 100644 packages/reversi/tsconfig.json diff --git a/packages/backend/migration/1705475608437-reversi.js b/packages/backend/migration/1705475608437-reversi.js new file mode 100644 index 0000000000..c9d69e2c7c --- /dev/null +++ b/packages/backend/migration/1705475608437-reversi.js @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class Reversi1705475608437 { + name = 'Reversi1705475608437' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_b46ec40746efceac604142be1c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b604d92d6c7aec38627f6eaf16"`); + await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "reversi_matching" DROP COLUMN "createdAt"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "reversi_matching" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); + await queryRunner.query(`ALTER TABLE "reversi_game" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_b604d92d6c7aec38627f6eaf16" ON "reversi_matching" ("createdAt") `); + await queryRunner.query(`CREATE INDEX "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt") `); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 5ab476295c..ef83be0b01 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -107,6 +107,7 @@ "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.24.3", diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index e29fee3f96..841a9e9bb6 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -79,5 +79,7 @@ export const DI = { flashLikesRepository: Symbol('flashLikesRepository'), userMemosRepository: Symbol('userMemosRepository'), bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), + reversiGamesRepository: Symbol('reversiGamesRepository'), + reversiMatchingsRepository: Symbol('reversiMatchingsRepository'), //#endregion }; diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 0399536c3e..f2c4e29112 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -5,7 +5,7 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord } from './_.js'; +import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame, MiReversiMatching } from './_.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -399,12 +399,24 @@ const $userMemosRepository: Provider = { inject: [DI.db], }; -export const $bubbleGameRecordsRepository: Provider = { +const $bubbleGameRecordsRepository: Provider = { provide: DI.bubbleGameRecordsRepository, useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord), inject: [DI.db], }; +const $reversiGamesRepository: Provider = { + provide: DI.reversiGamesRepository, + useFactory: (db: DataSource) => db.getRepository(MiReversiGame), + inject: [DI.db], +}; + +const $reversiMatchingsRepository: Provider = { + provide: DI.reversiMatchingsRepository, + useFactory: (db: DataSource) => db.getRepository(MiReversiMatching), + inject: [DI.db], +}; + @Module({ imports: [ ], @@ -475,6 +487,8 @@ export const $bubbleGameRecordsRepository: Provider = { $flashLikesRepository, $userMemosRepository, $bubbleGameRecordsRepository, + $reversiGamesRepository, + $reversiMatchingsRepository, ], exports: [ $usersRepository, @@ -543,6 +557,8 @@ export const $bubbleGameRecordsRepository: Provider = { $flashLikesRepository, $userMemosRepository, $bubbleGameRecordsRepository, + $reversiGamesRepository, + $reversiMatchingsRepository, ], }) export class RepositoryModule {} diff --git a/packages/backend/src/models/ReversiGame.ts b/packages/backend/src/models/ReversiGame.ts new file mode 100644 index 0000000000..d8c9f00132 --- /dev/null +++ b/packages/backend/src/models/ReversiGame.ts @@ -0,0 +1,127 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('reversi_game') +export class MiReversiGame { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + nullable: true, + comment: 'The started date of the ReversiGame.', + }) + public startedAt: Date | null; + + @Column(id()) + public user1Id: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user1: MiUser | null; + + @Column(id()) + public user2Id: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user2: MiUser | null; + + @Column('boolean', { + default: false, + }) + public user1Accepted: boolean; + + @Column('boolean', { + default: false, + }) + public user2Accepted: boolean; + + /** + * どちらのプレイヤーが先行(黒)か + * 1 ... user1 + * 2 ... user2 + */ + @Column('integer', { + nullable: true, + }) + public black: number | null; + + @Column('boolean', { + default: false, + }) + public isStarted: boolean; + + @Column('boolean', { + default: false, + }) + public isEnded: boolean; + + @Column({ + ...id(), + nullable: true, + }) + public winnerId: MiUser['id'] | null; + + @Column({ + ...id(), + nullable: true, + }) + public surrendered: MiUser['id'] | null; + + @Column('jsonb', { + default: [], + }) + public logs: { + at: Date; + color: boolean; + pos: number; + }[]; + + @Column('varchar', { + array: true, length: 64, + }) + public map: string[]; + + @Column('varchar', { + length: 32, + }) + public bw: string; + + @Column('boolean', { + default: false, + }) + public isLlotheo: boolean; + + @Column('boolean', { + default: false, + }) + public canPutEverywhere: boolean; + + @Column('boolean', { + default: false, + }) + public loopedBoard: boolean; + + @Column('jsonb', { + nullable: true, default: null, + }) + public form1: any | null; + + @Column('jsonb', { + nullable: true, default: null, + }) + public form2: any | null; + + /** + * ログのposを文字列としてすべて連結したもののCRC32値 + */ + @Column('varchar', { + length: 32, nullable: true, + }) + public crc32: string | null; +} diff --git a/packages/backend/src/models/ReversiMatching.ts b/packages/backend/src/models/ReversiMatching.ts new file mode 100644 index 0000000000..1e31fca901 --- /dev/null +++ b/packages/backend/src/models/ReversiMatching.ts @@ -0,0 +1,29 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('reversi_matching') +export class MiReversiMatching { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column(id()) + public parentId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public parent: MiUser | null; + + @Index() + @Column(id()) + public childId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public child: MiUser | null; +} diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index a1c4b0743e..0b097caf95 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -69,6 +69,9 @@ import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; +import { MiReversiGame } from '@/models/ReversiGame.js'; +import { MiReversiMatching } from '@/models/ReversiMatching.js'; + import type { Repository } from 'typeorm'; export { @@ -138,6 +141,8 @@ export { MiFlashLike, MiUserMemo, MiBubbleGameRecord, + MiReversiGame, + MiReversiMatching, }; export type AbuseUserReportsRepository = Repository; @@ -206,3 +211,5 @@ export type FlashsRepository = Repository; export type FlashLikesRepository = Repository; export type UserMemoRepository = Repository; export type BubbleGameRecordsRepository = Repository; +export type ReversiGamesRepository = Repository; +export type ReversiMatchingsRepository = Repository; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 0430e9ca19..5435696b14 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -77,6 +77,8 @@ import { MiFlash } from '@/models/Flash.js'; import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserMemo } from '@/models/UserMemo.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; +import { MiReversiGame } from '@/models/ReversiGame.js'; +import { MiReversiMatching } from '@/models/ReversiMatching.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; @@ -192,6 +194,8 @@ export const entities = [ MiFlashLike, MiUserMemo, MiBubbleGameRecord, + MiReversiGame, + MiReversiMatching, ...charts, ]; diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts new file mode 100644 index 0000000000..4b900d8e89 --- /dev/null +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -0,0 +1,359 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import * as CRC32 from 'crc-32'; +import type { MiReversiGame, MiUser, ReversiGamesRepository } from '@/models/_.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import Channel, { type MiChannelService } from '../channel.js'; + +class ReversiGameChannel extends Channel { + public readonly chName = 'reversiGame'; + public static shouldShare = false; + public static requireCredential = false as const; + private gameId: MiReversiGame['id'] | null = null; + + constructor( + private reversiGamesRepository: ReversiGamesRepository, + + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + } + + @bindThis + public async init(params: any) { + this.gameId = params.gameId as string; + + const game = await this.reversiGamesRepository.findOneBy({ + id: this.gameId, + }); + if (game == null) return; + + this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send); + } + + @bindThis + public onMessage(type: string, body: any) { + switch (type) { + case 'accept': this.accept(true); break; + case 'cancelAccept': this.accept(false); break; + case 'updateSettings': this.updateSettings(body.key, body.value); break; + case 'initForm': this.initForm(body); break; + case 'updateForm': this.updateForm(body.id, body.value); break; + case 'message': this.message(body); break; + case 'set': this.set(body.pos); break; + case 'check': this.check(body.crc32); break; + } + } + + @bindThis + private async updateSettings(key: string, value: any) { + if (this.user == null) return; + + const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); + if (game == null) throw new Error('game not found'); + + if (game.isStarted) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; + if ((game.user1Id === this.user.id) && game.user1Accepted) return; + if ((game.user2Id === this.user.id) && game.user2Accepted) return; + + if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return; + + await this.reversiGamesRepository.update(this.gameId!, { + [key]: value, + }); + + publishReversiGameStream(this.gameId!, 'updateSettings', { + key: key, + value: value, + }); + } + + @bindThis + private async initForm(form: any) { + if (this.user == null) return; + + const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); + if (game == null) throw new Error('game not found'); + + if (game.isStarted) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; + + const set = game.user1Id === this.user.id ? { + form1: form, + } : { + form2: form, + }; + + await this.reversiGamesRepository.update(this.gameId!, set); + + publishReversiGameStream(this.gameId!, 'initForm', { + userId: this.user.id, + form, + }); + } + + @bindThis + private async updateForm(id: string, value: any) { + if (this.user == null) return; + + const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); + if (game == null) throw new Error('game not found'); + + if (game.isStarted) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; + + const form = game.user1Id === this.user.id ? game.form2 : game.form1; + + const item = form.find((i: any) => i.id == id); + + if (item == null) return; + + item.value = value; + + const set = game.user1Id === this.user.id ? { + form2: form, + } : { + form1: form, + }; + + await this.reversiGamesRepository.update(this.gameId!, set); + + publishReversiGameStream(this.gameId!, 'updateForm', { + userId: this.user.id, + id, + value, + }); + } + + @bindThis + private async message(message: any) { + if (this.user == null) return; + + message.id = Math.random(); + publishReversiGameStream(this.gameId!, 'message', { + userId: this.user.id, + message, + }); + } + + @bindThis + private async accept(accept: boolean) { + if (this.user == null) return; + + const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); + if (game == null) throw new Error('game not found'); + + if (game.isStarted) return; + + let bothAccepted = false; + + if (game.user1Id === this.user.id) { + await this.reversiGamesRepository.update(this.gameId!, { + user1Accepted: accept, + }); + + publishReversiGameStream(this.gameId!, 'changeAccepts', { + user1: accept, + user2: game.user2Accepted, + }); + + if (accept && game.user2Accepted) bothAccepted = true; + } else if (game.user2Id === this.user.id) { + await this.reversiGamesRepository.update(this.gameId!, { + user2Accepted: accept, + }); + + publishReversiGameStream(this.gameId!, 'changeAccepts', { + user1: game.user1Accepted, + user2: accept, + }); + + if (accept && game.user1Accepted) bothAccepted = true; + } else { + return; + } + + if (bothAccepted) { + // 3秒後、まだacceptされていたらゲーム開始 + setTimeout(async () => { + const freshGame = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); + if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; + if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; + + let bw: number; + if (freshGame.bw == 'random') { + bw = Math.random() > 0.5 ? 1 : 2; + } else { + bw = parseInt(freshGame.bw, 10); + } + + function getRandomMap() { + const mapCount = Object.entries(maps).length; + const rnd = Math.floor(Math.random() * mapCount); + return Object.values(maps)[rnd].data; + } + + const map = freshGame.map != null ? freshGame.map : getRandomMap(); + + await this.reversiGamesRepository.update(this.gameId!, { + startedAt: new Date(), + isStarted: true, + black: bw, + map: map, + }); + + //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 + const o = new ReversiGame(map, { + isLlotheo: freshGame.isLlotheo, + canPutEverywhere: freshGame.canPutEverywhere, + loopedBoard: freshGame.loopedBoard, + }); + + if (o.isEnded) { + let winner; + if (o.winner === true) { + winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; + } else if (o.winner === false) { + winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; + } else { + winner = null; + } + + await this.reversiGamesRepository.update(this.gameId!, { + isEnded: true, + winnerId: winner, + }); + + publishReversiGameStream(this.gameId!, 'ended', { + winnerId: winner, + game: await ReversiGames.pack(this.gameId!, this.user), + }); + } + //#endregion + + publishReversiGameStream(this.gameId!, 'started', + await ReversiGames.pack(this.gameId!, this.user)); + }, 3000); + } + } + + @bindThis + private async set(pos: number) { + if (this.user == null) return; + + const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); + if (game == null) throw new Error('game not found'); + + if (!game.isStarted) return; + if (game.isEnded) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; + + const myColor = + ((game.user1Id === this.user.id) && game.black === 1) || ((game.user2Id === this.user.id) && game.black === 2) + ? true + : false; + + const o = new ReversiGame(game.map, { + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + }); + + // 盤面の状態を再生 + for (const log of game.logs) { + o.put(log.color, log.pos); + } + + if (o.turn !== myColor) return; + + if (!o.canPut(myColor, pos)) return; + o.put(myColor, pos); + + let winner; + if (o.isEnded) { + if (o.winner === true) { + winner = game.black === 1 ? game.user1Id : game.user2Id; + } else if (o.winner === false) { + winner = game.black === 1 ? game.user2Id : game.user1Id; + } else { + winner = null; + } + } + + const log = { + at: new Date(), + color: myColor, + pos, + }; + + const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString(); + + game.logs.push(log); + + await this.reversiGamesRepository.update(this.gameId!, { + crc32, + isEnded: o.isEnded, + winnerId: winner, + logs: game.logs, + }); + + publishReversiGameStream(this.gameId!, 'set', Object.assign(log, { + next: o.turn, + })); + + if (o.isEnded) { + publishReversiGameStream(this.gameId!, 'ended', { + winnerId: winner, + game: await ReversiGames.pack(this.gameId!, this.user), + }); + } + } + + @bindThis + private async check(crc32: string | number) { + const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); + if (game == null) throw new Error('game not found'); + + if (!game.isStarted) return; + + if (crc32.toString() !== game.crc32) { + this.send('rescue', await ReversiGames.pack(game, this.user)); + } + } + + @bindThis + public dispose() { + // Unsubscribe events + this.subscriber.off(`reversiGameStream:${this.gameId}`, this.send); + } +} + +@Injectable() +export class ReversiGameChannelService implements MiChannelService { + public readonly shouldShare = ReversiGameChannel.shouldShare; + public readonly requireCredential = ReversiGameChannel.requireCredential; + public readonly kind = ReversiGameChannel.kind; + + constructor( + @Inject(DI.reversiGamesRepository) + private reversiGamesRepository: ReversiGamesRepository, + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): ReversiGameChannel { + return new ReversiGameChannel( + this.reversiGamesRepository, + id, + connection, + ); + } +} diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/reversi/package.json b/packages/reversi/package.json new file mode 100644 index 0000000000..0578a341c6 --- /dev/null +++ b/packages/reversi/package.json @@ -0,0 +1,26 @@ +{ + "name": "reversi", + "version": "0.0.1", + "main": "./built/index.js", + "types": "./built/index.d.ts", + "scripts": { + "build": "tsc", + "watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"", + "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", + "typecheck": "tsc --noEmit", + "lint": "pnpm typecheck && pnpm eslint" + }, + "devDependencies": { + "@misskey-dev/eslint-plugin": "^1.0.0", + "@types/node": "20.10.5", + "@typescript-eslint/eslint-plugin": "6.14.0", + "@typescript-eslint/parser": "6.14.0", + "eslint": "8.56.0", + "typescript": "5.3.3" + }, + "files": [ + "built" + ], + "dependencies": { + } +} diff --git a/packages/reversi/src/engine.ts b/packages/reversi/src/engine.ts new file mode 100644 index 0000000000..59fef8f587 --- /dev/null +++ b/packages/reversi/src/engine.ts @@ -0,0 +1,216 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * true ... 黒 + * false ... 白 + */ +export type Color = boolean; +const BLACK = true; +const WHITE = false; + +export type MapCell = 'null' | 'empty'; + +export type Options = { + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; +}; + +export type Undo = { + color: Color; + pos: number; + + /** + * 反転した石の位置の配列 + */ + effects: number[]; + + turn: Color | null; +}; + +export class ReversiGame { + public map: MapCell[]; + public mapWidth: number; + public mapHeight: number; + public board: (Color | null | undefined)[]; + public turn: Color | null = BLACK; + public opts: Options; + + public prevPos = -1; + public prevColor: Color | null = null; + + private logs: Undo[] = []; + + constructor(map: string[], opts: Options) { + //#region binds + this.put = this.put.bind(this); + //#endregion + + //#region Options + this.opts = opts; + if (this.opts.isLlotheo == null) this.opts.isLlotheo = false; + if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false; + if (this.opts.loopedBoard == null) this.opts.loopedBoard = false; + //#endregion + + //#region Parse map data + this.mapWidth = map[0].length; + this.mapHeight = map.length; + const mapData = map.join(''); + + this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined); + + this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null'); + //#endregion + + // ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある + if (!this.canPutSomewhere(BLACK)) + this.turn = this.canPutSomewhere(WHITE) ? WHITE : null; + } + + public get blackCount() { + return this.board.filter(x => x === BLACK).length; + } + + public get whiteCount() { + return this.board.filter(x => x === WHITE).length; + } + + public transformPosToXy(pos: number): number[] { + const x = pos % this.mapWidth; + const y = Math.floor(pos / this.mapWidth); + return [x, y]; + } + + public transformXyToPos(x: number, y: number): number { + return x + (y * this.mapWidth); + } + + public put(color: Color, pos: number) { + this.prevPos = pos; + this.prevColor = color; + + this.board[pos] = color; + + // 反転させられる石を取得 + const effects = this.effects(color, pos); + + // 反転させる + for (const pos of effects) { + this.board[pos] = color; + } + + const turn = this.turn; + + this.logs.push({ + color, + pos, + effects, + turn + }); + + this.calcTurn(); + } + + private calcTurn() { + // ターン計算 + this.turn = + this.canPutSomewhere(!this.prevColor) ? !this.prevColor : + this.canPutSomewhere(this.prevColor!) ? this.prevColor : + null; + } + + public undo() { + const undo = this.logs.pop()!; + this.prevColor = undo.color; + this.prevPos = undo.pos; + this.board[undo.pos] = null; + for (const pos of undo.effects) { + const color = this.board[pos]; + this.board[pos] = !color; + } + this.turn = undo.turn; + } + + public mapDataGet(pos: number): MapCell { + const [x, y] = this.transformPosToXy(pos); + return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos]; + } + + public getPuttablePlaces(color: Color): number[] { + return Array.from(this.board.keys()).filter(i => this.canPut(color, i)); + } + + public canPutSomewhere(color: Color): boolean { + return this.getPuttablePlaces(color).length > 0; + } + + public canPut(color: Color, pos: number): boolean { + return ( + this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない + this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード + this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか + } + + /** + * 指定のマスに石を置いた時の、反転させられる石を取得します + * @param color 自分の色 + * @param initPos 位置 + */ + public effects(color: Color, initPos: number): number[] { + const enemyColor = !color; + + const diffVectors: [number, number][] = [ + [ 0, -1], // 上 + [ +1, -1], // 右上 + [ +1, 0], // 右 + [ +1, +1], // 右下 + [ 0, +1], // 下 + [ -1, +1], // 左下 + [ -1, 0], // 左 + [ -1, -1] // 左上 + ]; + + const effectsInLine = ([dx, dy]: [number, number]): number[] => { + const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy]; + + const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列 + let [x, y] = this.transformPosToXy(initPos); + while (true) { + [x, y] = nextPos(x, y); + + // 座標が指し示す位置がボード外に出たとき + if (this.opts.loopedBoard && this.transformXyToPos( + (x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth), + (y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos) + // 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ) + return found; + else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight) + return []; // 挟めないことが確定 (盤面外に到達) + + const pos = this.transformXyToPos(x, y); + if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達) + const stone = this.board[pos]; + if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達) + if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見) + if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見) + } + }; + + return ([] as number[]).concat(...diffVectors.map(effectsInLine)); + } + + public get isEnded(): boolean { + return this.turn === null; + } + + public get winner(): Color | null { + return this.isEnded ? + this.blackCount == this.whiteCount ? null : + this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK : + undefined as never; + } +} diff --git a/packages/reversi/src/maps.ts b/packages/reversi/src/maps.ts new file mode 100644 index 0000000000..85cf1a0485 --- /dev/null +++ b/packages/reversi/src/maps.ts @@ -0,0 +1,715 @@ +/** + * 組み込みマップ定義 + * + * データ値: + * (スペース) ... マス無し + * - ... マス + * b ... 初期配置される黒石 + * w ... 初期配置される白石 + */ + +export type Map = { + name?: string; + category?: string; + author?: string; + data: string[]; +}; + +export const fourfour: Map = { + name: '4x4', + category: '4x4', + data: [ + '----', + '-wb-', + '-bw-', + '----' + ] +}; + +export const sixsix: Map = { + name: '6x6', + category: '6x6', + data: [ + '------', + '------', + '--wb--', + '--bw--', + '------', + '------' + ] +}; + +export const roundedSixsix: Map = { + name: '6x6 rounded', + category: '6x6', + author: 'syuilo', + data: [ + ' ---- ', + '------', + '--wb--', + '--bw--', + '------', + ' ---- ' + ] +}; + +export const roundedSixsix2: Map = { + name: '6x6 rounded 2', + category: '6x6', + author: 'syuilo', + data: [ + ' -- ', + ' ---- ', + '--wb--', + '--bw--', + ' ---- ', + ' -- ' + ] +}; + +export const eighteight: Map = { + name: '8x8', + category: '8x8', + data: [ + '--------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const eighteightH28: Map = { + name: '8x8 handicap 28', + category: '8x8', + data: [ + 'bbbbbbbb', + 'b------b', + 'b------b', + 'b--wb--b', + 'b--bw--b', + 'b------b', + 'b------b', + 'bbbbbbbb' + ] +}; + +export const roundedEighteight: Map = { + name: '8x8 rounded', + category: '8x8', + author: 'syuilo', + data: [ + ' ------ ', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + ' ------ ' + ] +}; + +export const roundedEighteight2: Map = { + name: '8x8 rounded 2', + category: '8x8', + author: 'syuilo', + data: [ + ' ---- ', + ' ------ ', + '--------', + '---wb---', + '---bw---', + '--------', + ' ------ ', + ' ---- ' + ] +}; + +export const roundedEighteight3: Map = { + name: '8x8 rounded 3', + category: '8x8', + author: 'syuilo', + data: [ + ' -- ', + ' ---- ', + ' ------ ', + '---wb---', + '---bw---', + ' ------ ', + ' ---- ', + ' -- ' + ] +}; + +export const eighteightWithNotch: Map = { + name: '8x8 with notch', + category: '8x8', + author: 'syuilo', + data: [ + '--- ---', + '--------', + '--------', + ' --wb-- ', + ' --bw-- ', + '--------', + '--------', + '--- ---' + ] +}; + +export const eighteightWithSomeHoles: Map = { + name: '8x8 with some holes', + category: '8x8', + author: 'syuilo', + data: [ + '--- ----', + '----- --', + '-- -----', + '---wb---', + '---bw- -', + ' -------', + '--- ----', + '--------' + ] +}; + +export const circle: Map = { + name: 'Circle', + category: '8x8', + author: 'syuilo', + data: [ + ' -- ', + ' ------ ', + ' ------ ', + '---wb---', + '---bw---', + ' ------ ', + ' ------ ', + ' -- ' + ] +}; + +export const smile: Map = { + name: 'Smile', + category: '8x8', + author: 'syuilo', + data: [ + ' ------ ', + '--------', + '-- -- --', + '---wb---', + '-- bw --', + '--- ---', + '--------', + ' ------ ' + ] +}; + +export const window: Map = { + name: 'Window', + category: '8x8', + author: 'syuilo', + data: [ + '--------', + '- -- -', + '- -- -', + '---wb---', + '---bw---', + '- -- -', + '- -- -', + '--------' + ] +}; + +export const reserved: Map = { + name: 'Reserved', + category: '8x8', + author: 'Aya', + data: [ + 'w------b', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + 'b------w' + ] +}; + +export const x: Map = { + name: 'X', + category: '8x8', + author: 'Aya', + data: [ + 'w------b', + '-w----b-', + '--w--b--', + '---wb---', + '---bw---', + '--b--w--', + '-b----w-', + 'b------w' + ] +}; + +export const parallel: Map = { + name: 'Parallel', + category: '8x8', + author: 'Aya', + data: [ + '--------', + '--------', + '--------', + '---bb---', + '---ww---', + '--------', + '--------', + '--------' + ] +}; + +export const lackOfBlack: Map = { + name: 'Lack of Black', + category: '8x8', + data: [ + '--------', + '--------', + '--------', + '---w----', + '---bw---', + '--------', + '--------', + '--------' + ] +}; + +export const squareParty: Map = { + name: 'Square Party', + category: '8x8', + author: 'syuilo', + data: [ + '--------', + '-wwwbbb-', + '-w-wb-b-', + '-wwwbbb-', + '-bbbwww-', + '-b-bw-w-', + '-bbbwww-', + '--------' + ] +}; + +export const minesweeper: Map = { + name: 'Minesweeper', + category: '8x8', + author: 'syuilo', + data: [ + 'b-b--w-w', + '-w-wb-b-', + 'w-b--w-b', + '-b-wb-w-', + '-w-bw-b-', + 'b-w--b-w', + '-b-bw-w-', + 'w-w--b-b' + ] +}; + +export const tenthtenth: Map = { + name: '10x10', + category: '10x10', + data: [ + '----------', + '----------', + '----------', + '----------', + '----wb----', + '----bw----', + '----------', + '----------', + '----------', + '----------' + ] +}; + +export const hole: Map = { + name: 'The Hole', + category: '10x10', + author: 'syuilo', + data: [ + '----------', + '----------', + '--wb--wb--', + '--bw--bw--', + '---- ----', + '---- ----', + '--wb--wb--', + '--bw--bw--', + '----------', + '----------' + ] +}; + +export const grid: Map = { + name: 'Grid', + category: '10x10', + author: 'syuilo', + data: [ + '----------', + '- - -- - -', + '----------', + '- - -- - -', + '----wb----', + '----bw----', + '- - -- - -', + '----------', + '- - -- - -', + '----------' + ] +}; + +export const cross: Map = { + name: 'Cross', + category: '10x10', + author: 'Aya', + data: [ + ' ---- ', + ' ---- ', + ' ---- ', + '----------', + '----wb----', + '----bw----', + '----------', + ' ---- ', + ' ---- ', + ' ---- ' + ] +}; + +export const charX: Map = { + name: 'Char X', + category: '10x10', + author: 'syuilo', + data: [ + '--- ---', + '---- ----', + '----------', + ' -------- ', + ' --wb-- ', + ' --bw-- ', + ' -------- ', + '----------', + '---- ----', + '--- ---' + ] +}; + +export const charY: Map = { + name: 'Char Y', + category: '10x10', + author: 'syuilo', + data: [ + '--- ---', + '---- ----', + '----------', + ' -------- ', + ' --wb-- ', + ' --bw-- ', + ' ------ ', + ' ------ ', + ' ------ ', + ' ------ ' + ] +}; + +export const walls: Map = { + name: 'Walls', + category: '10x10', + author: 'Aya', + data: [ + ' bbbbbbbb ', + 'w--------w', + 'w--------w', + 'w--------w', + 'w---wb---w', + 'w---bw---w', + 'w--------w', + 'w--------w', + 'w--------w', + ' bbbbbbbb ' + ] +}; + +export const cpu: Map = { + name: 'CPU', + category: '10x10', + author: 'syuilo', + data: [ + ' b b b b ', + 'w--------w', + ' -------- ', + 'w--------w', + ' ---wb--- ', + ' ---bw--- ', + 'w--------w', + ' -------- ', + 'w--------w', + ' b b b b ' + ] +}; + +export const checker: Map = { + name: 'Checker', + category: '10x10', + author: 'Aya', + data: [ + '----------', + '----------', + '----------', + '---wbwb---', + '---bwbw---', + '---wbwb---', + '---bwbw---', + '----------', + '----------', + '----------' + ] +}; + +export const japaneseCurry: Map = { + name: 'Japanese curry', + category: '10x10', + author: 'syuilo', + data: [ + 'w-b-b-b-b-', + '-w-b-b-b-b', + 'w-w-b-b-b-', + '-w-w-b-b-b', + 'w-w-wwb-b-', + '-w-wbb-b-b', + 'w-w-w-b-b-', + '-w-w-w-b-b', + 'w-w-w-w-b-', + '-w-w-w-w-b' + ] +}; + +export const mosaic: Map = { + name: 'Mosaic', + category: '10x10', + author: 'syuilo', + data: [ + '- - - - - ', + ' - - - - -', + '- - - - - ', + ' - w w - -', + '- - b b - ', + ' - w w - -', + '- - b b - ', + ' - - - - -', + '- - - - - ', + ' - - - - -', + ] +}; + +export const arena: Map = { + name: 'Arena', + category: '10x10', + author: 'syuilo', + data: [ + '- - -- - -', + ' - - - - ', + '- ------ -', + ' -------- ', + '- --wb-- -', + '- --bw-- -', + ' -------- ', + '- ------ -', + ' - - - - ', + '- - -- - -' + ] +}; + +export const reactor: Map = { + name: 'Reactor', + category: '10x10', + author: 'syuilo', + data: [ + '-w------b-', + 'b- - - -w', + '- --wb-- -', + '---b w---', + '- b wb w -', + '- w bw b -', + '---w b---', + '- --bw-- -', + 'w- - - -b', + '-b------w-' + ] +}; + +export const sixeight: Map = { + name: '6x8', + category: 'Special', + data: [ + '------', + '------', + '------', + '--wb--', + '--bw--', + '------', + '------', + '------' + ] +}; + +export const spark: Map = { + name: 'Spark', + category: 'Special', + author: 'syuilo', + data: [ + ' - - ', + '----------', + ' -------- ', + ' -------- ', + ' ---wb--- ', + ' ---bw--- ', + ' -------- ', + ' -------- ', + '----------', + ' - - ' + ] +}; + +export const islands: Map = { + name: 'Islands', + category: 'Special', + author: 'syuilo', + data: [ + '-------- ', + '---wb--- ', + '---bw--- ', + '-------- ', + ' - - ', + ' - - ', + ' --------', + ' --------', + ' --------', + ' --------' + ] +}; + +export const galaxy: Map = { + name: 'Galaxy', + category: 'Special', + author: 'syuilo', + data: [ + ' ------ ', + ' --www--- ', + ' ------w--- ', + '---bbb--w---', + '--b---b-w-b-', + '-b--wwb-w-b-', + '-b-w-bww--b-', + '-b-w-b---b--', + '---w--bbb---', + ' ---w------ ', + ' ---www-- ', + ' ------ ' + ] +}; + +export const triangle: Map = { + name: 'Triangle', + category: 'Special', + author: 'syuilo', + data: [ + ' -- ', + ' -- ', + ' ---- ', + ' ---- ', + ' --wb-- ', + ' --bw-- ', + ' -------- ', + ' -------- ', + '----------', + '----------' + ] +}; + +export const iphonex: Map = { + name: 'iPhone X', + category: 'Special', + author: 'syuilo', + data: [ + ' -- -- ', + '--------', + '--------', + '--------', + '--------', + '---wb---', + '---bw---', + '--------', + '--------', + '--------', + '--------', + ' ------ ' + ] +}; + +export const dealWithIt: Map = { + name: 'Deal with it!', + category: 'Special', + author: 'syuilo', + data: [ + '------------', + '--w-b-------', + ' --b-w------', + ' --w-b---- ', + ' ------- ' + ] +}; + +export const bigBoard: Map = { + name: 'Big board', + category: 'Special', + data: [ + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '-------wb-------', + '-------bw-------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------', + '----------------' + ] +}; + +export const twoBoard: Map = { + name: 'Two board', + category: 'Special', + author: 'Aya', + data: [ + '-------- --------', + '-------- --------', + '-------- --------', + '---wb--- ---wb---', + '---bw--- ---bw---', + '-------- --------', + '-------- --------', + '-------- --------' + ] +}; diff --git a/packages/reversi/tsconfig.json b/packages/reversi/tsconfig.json new file mode 100644 index 0000000000..f56b65e868 --- /dev/null +++ b/packages/reversi/tsconfig.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./built/", + "removeComments": true, + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "esModuleInterop": true, + "typeRoots": [ + "./node_modules/@types" + ], + "lib": [ + "esnext", + "dom" + ] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "test/**/*" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 825a7ab860..4fc99c483e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,6 +185,9 @@ 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 From d6c8e520f84ca77cdf2d955f2950b2be01a3371c Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 17 Jan 2024 20:44:02 +0900 Subject: [PATCH 0003/1120] wip --- .../api/endpoints/renote-mute/create.ts | 2 +- .../src/server/api/endpoints/reversi/match.ts | 119 ++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/server/api/endpoints/reversi/match.ts diff --git a/packages/backend/src/server/api/endpoints/renote-mute/create.ts b/packages/backend/src/server/api/endpoints/renote-mute/create.ts index 7ff7b5de3a..2d853b94f3 100644 --- a/packages/backend/src/server/api/endpoints/renote-mute/create.ts +++ b/packages/backend/src/server/api/endpoints/renote-mute/create.ts @@ -73,7 +73,7 @@ export default class extends Endpoint { // eslint- } // Get mutee - const mutee = await getterService.getUser(ps.userId).catch(err => { + const mutee = await this.getterService.getUser(ps.userId).catch(err => { if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); throw err; }); diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts new file mode 100644 index 0000000000..86d3ad6da4 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { IdService } from '@/core/IdService.js'; +import type { ReversiGamesRepository, ReversiMatchingsRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; +import { GetterService } from '../../GetterService.js'; + +export const meta = { + requireCredential: true, + + kind: 'write:account', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '0b4f0559-b484-4e31-9581-3f73cee89b28', + }, + + isYourself: { + message: 'Target user is yourself.', + code: 'TARGET_IS_YOURSELF', + id: '96fd7bd6-d2bc-426c-a865-d055dcd2828e', + }, + }, + + res: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.reversiGamesRepository) + private reversiGamesRepository: ReversiGamesRepository, + + @Inject(DI.reversiMatchingsRepository) + private reversiMatchingsRepository: ReversiMatchingsRepository, + + private getterService: GetterService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + if (ps.userId === me.id) { + throw new ApiError(meta.errors.isYourself); + } + + const exist = await this.reversiMatchingsRepository.findOneBy({ + parentId: ps.userId, + childId: me.id, + }); + + if (exist) { + this.reversiMatchingsRepository.delete(exist.id); + + const game = await this.reversiGamesRepository.insert({ + id: this.idService.gen(), + user1Id: exist.parentId, + user2Id: me.id, + user1Accepted: false, + user2Accepted: false, + isStarted: false, + isEnded: false, + logs: [], + map: eighteight.data, + bw: 'random', + isLlotheo: false, + }); + + publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, { id: exist.parentId })); + + const other = await this.reversiMatchingsRepository.countBy({ + childId: me.id, + }); + + if (other == 0) { + publishMainStream(me.id, 'reversiNoInvites'); + } + + return await ReversiGames.pack(game, me); + } else { + const child = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + await this.reversiMatchingsRepository.delete({ + parentId: me.id, + }); + + const matching = await this.reversiMatchingsRepository.insert({ + id: this.idService.gen(), + parentId: me.id, + childId: child.id, + }); + + const packed = await ReversiMatchings.pack(matching, child); + publishReversiStream(child.id, 'invited', packed); + publishMainStream(child.id, 'reversiInvited', packed); + + return; + } + }); + } +} From 22bb79b3fd195ed66ec772446da88e1f24af8419 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 11:47:21 +0900 Subject: [PATCH 0004/1120] wip --- packages/backend/src/core/CoreModule.ts | 33 +++++ packages/backend/src/core/ReversiService.ts | 126 ++++++++++++++++ .../core/entities/ReversiGameEntityService.ts | 81 +++++++++++ .../entities/ReversiMatchingEntityService.ts | 57 ++++++++ packages/backend/src/misc/json-schema.ts | 4 + .../src/models/json-schema/reversi-game.ts | 135 ++++++++++++++++++ .../models/json-schema/reversi-matching.ts | 40 ++++++ 7 files changed, 476 insertions(+) create mode 100644 packages/backend/src/core/ReversiService.ts create mode 100644 packages/backend/src/core/entities/ReversiGameEntityService.ts create mode 100644 packages/backend/src/core/entities/ReversiMatchingEntityService.ts create mode 100644 packages/backend/src/models/json-schema/reversi-game.ts create mode 100644 packages/backend/src/models/json-schema/reversi-matching.ts diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index bc6d24b951..0ad91ff308 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -66,6 +66,8 @@ import { FeaturedService } from './FeaturedService.js'; import { FanoutTimelineService } from './FanoutTimelineService.js'; import { ChannelFollowingService } from './ChannelFollowingService.js'; import { RegistryApiService } from './RegistryApiService.js'; +import { ReversiService } from './ReversiService.js'; + import { ChartLoggerService } from './chart/ChartLoggerService.js'; import FederationChart from './chart/charts/federation.js'; import NotesChart from './chart/charts/notes.js'; @@ -80,6 +82,7 @@ import PerUserFollowingChart from './chart/charts/per-user-following.js'; import PerUserDriveChart from './chart/charts/per-user-drive.js'; import ApRequestChart from './chart/charts/ap-request.js'; import { ChartManagementService } from './chart/ChartManagementService.js'; + import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js'; import { AntennaEntityService } from './entities/AntennaEntityService.js'; import { AppEntityService } from './entities/AppEntityService.js'; @@ -112,6 +115,9 @@ import { UserListEntityService } from './entities/UserListEntityService.js'; import { FlashEntityService } from './entities/FlashEntityService.js'; import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js'; import { RoleEntityService } from './entities/RoleEntityService.js'; +import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; +import { ReversiMatchingEntityService } from './entities/ReversiMatchingEntityService.js'; + import { ApAudienceService } from './activitypub/ApAudienceService.js'; import { ApDbResolverService } from './activitypub/ApDbResolverService.js'; import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js'; @@ -199,6 +205,7 @@ const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', use const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService }; const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService }; const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService }; +const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; @@ -247,6 +254,8 @@ const $UserListEntityService: Provider = { provide: 'UserListEntityService', use const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; +const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService }; +const $ReversiMatchingEntityService: Provider = { provide: 'ReversiMatchingEntityService', useExisting: ReversiMatchingEntityService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; @@ -336,6 +345,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, + ReversiService, + ChartLoggerService, FederationChart, NotesChart, @@ -350,6 +361,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PerUserDriveChart, ApRequestChart, ChartManagementService, + AbuseUserReportEntityService, AntennaEntityService, AppEntityService, @@ -382,6 +394,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FlashEntityService, FlashLikeEntityService, RoleEntityService, + ReversiGameEntityService, + ReversiMatchingEntityService, + ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -466,6 +481,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, + $ReversiService, + $ChartLoggerService, $FederationChart, $NotesChart, @@ -480,6 +497,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PerUserDriveChart, $ApRequestChart, $ChartManagementService, + $AbuseUserReportEntityService, $AntennaEntityService, $AppEntityService, @@ -512,6 +530,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, + $ReversiGameEntityService, + $ReversiMatchingEntityService, + $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, @@ -597,6 +618,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, + ReversiService, + FederationChart, NotesChart, UsersChart, @@ -610,6 +633,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PerUserDriveChart, ApRequestChart, ChartManagementService, + AbuseUserReportEntityService, AntennaEntityService, AppEntityService, @@ -642,6 +666,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FlashEntityService, FlashLikeEntityService, RoleEntityService, + ReversiGameEntityService, + ReversiMatchingEntityService, + ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -726,6 +753,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, + $ReversiService, + $FederationChart, $NotesChart, $UsersChart, @@ -739,6 +768,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PerUserDriveChart, $ApRequestChart, $ChartManagementService, + $AbuseUserReportEntityService, $AntennaEntityService, $AppEntityService, @@ -771,6 +801,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, + $ReversiGameEntityService, + $ReversiMatchingEntityService, + $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts new file mode 100644 index 0000000000..64c0e25334 --- /dev/null +++ b/packages/backend/src/core/ReversiService.ts @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; +import { In } from 'typeorm'; +import { ModuleRef } from '@nestjs/core'; +import type { + MiReversiGame, + MiRole, + MiRoleAssignment, + RoleAssignmentsRepository, + RolesRepository, + UsersRepository, +} from '@/models/_.js'; +import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; +import type { MiUser } from '@/models/User.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { MetaService } from '@/core/MetaService.js'; +import { CacheService } from '@/core/CacheService.js'; +import type { RoleCondFormulaValue } from '@/models/Role.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { IdService } from '@/core/IdService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; + +@Injectable() +export class ReversiService implements OnApplicationShutdown, OnModuleInit { + private notificationService: NotificationService; + + constructor( + private moduleRef: ModuleRef, + + @Inject(DI.redis) + private redisClient: Redis.Redis, + + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private metaService: MetaService, + private cacheService: CacheService, + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private idService: IdService, + private moderationLogService: ModerationLogService, + private fanoutTimelineService: FanoutTimelineService, + ) { + } + + async onModuleInit() { + this.notificationService = this.moduleRef.get(NotificationService.name); + } + + @bindThis + public async getModerators(includeAdmins = true): Promise { + const ids = await this.getModeratorIds(includeAdmins); + const users = ids.length > 0 ? await this.usersRepository.findBy({ + id: In(ids), + }) : []; + return users; + } + + @bindThis + public async create(values: Partial, moderator?: MiUser): Promise { + const date = new Date(); + const created = await this.rolesRepository.insert({ + id: this.idService.gen(date.getTime()), + updatedAt: date, + lastUsedAt: date, + name: values.name, + description: values.description, + color: values.color, + iconUrl: values.iconUrl, + target: values.target, + condFormula: values.condFormula, + isPublic: values.isPublic, + isAdministrator: values.isAdministrator, + isModerator: values.isModerator, + isExplorable: values.isExplorable, + asBadge: values.asBadge, + canEditMembersByModerator: values.canEditMembersByModerator, + displayOrder: values.displayOrder, + policies: values.policies, + }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); + + this.globalEventService.publishInternalEvent('roleCreated', created); + + if (moderator) { + this.moderationLogService.log(moderator, 'createRole', { + roleId: created.id, + role: created, + }); + } + + return created; + } + + @bindThis + public dispose(): void { + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts new file mode 100644 index 0000000000..0bf02990d7 --- /dev/null +++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { ReversiGamesRepository } from '@/models/_.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/json-schema.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiReversiGame } from '@/models/ReversiGame.js'; +import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class ReversiGameEntityService { + constructor( + @Inject(DI.reversiGamesRepository) + private reversiGamesRepository: ReversiGamesRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + ) { + } + + @bindThis + public async pack( + src: MiReversiGame['id'] | MiReversiGame, + me?: { id: MiUser['id'] } | null | undefined, + options?: { + detail?: boolean; + skipHide?: boolean; + }, + ): Promise> { + const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: game.id, + createdAt: this.idService.parse(game.id).date.toISOString(), + startedAt: game.startedAt && game.startedAt.toISOString(), + isStarted: game.isStarted, + isEnded: game.isEnded, + form1: game.form1, + form2: game.form2, + user1Accepted: game.user1Accepted, + user2Accepted: game.user2Accepted, + user1Id: game.user1Id, + user2Id: game.user2Id, + user1: this.userEntityService.pack(game.user1Id, me), + user2: this.userEntityService.pack(game.user2Id, me), + winnerId: game.winnerId, + winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null, + surrendered: game.surrendered, + black: game.black, + bw: game.bw, + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + ...(options?.detail ? { + logs: game.logs.map(log => ({ + at: log.at.toISOString(), + color: log.color, + pos: log.pos, + })), + map: game.map, + } : {}), + }); + } + + @bindThis + public packMany( + xs: MiReversiGame[], + me?: { id: MiUser['id'] } | null | undefined, + ) { + return Promise.all(xs.map(x => this.pack(x, me))); + } +} + diff --git a/packages/backend/src/core/entities/ReversiMatchingEntityService.ts b/packages/backend/src/core/entities/ReversiMatchingEntityService.ts new file mode 100644 index 0000000000..1b39679ec1 --- /dev/null +++ b/packages/backend/src/core/entities/ReversiMatchingEntityService.ts @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { MiReversiMatching, ReversiMatchingsRepository } from '@/models/_.js'; +import { awaitAll } from '@/misc/prelude/await-all.js'; +import type { Packed } from '@/misc/json-schema.js'; +import type { } from '@/models/Blocking.js'; +import type { MiUser } from '@/models/User.js'; +import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class ReversiMatchingEntityService { + constructor( + @Inject(DI.reversiMatchingsRepository) + private reversiMatchingsRepository: ReversiMatchingsRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + ) { + } + + @bindThis + public async pack( + src: MiReversiMatching['id'] | MiReversiMatching, + me?: { id: MiUser['id'] } | null | undefined, + ): Promise> { + const matching = typeof src === 'object' ? src : await this.reversiMatchingsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: matching.id, + createdAt: this.idService.parse(matching.id).date.toISOString(), + parentId: matching.parentId, + parent: this.userEntityService.pack(matching.parentId, me, { + detail: true, + }), + childId: matching.childId, + child: this.userEntityService.pack(matching.childId, me, { + detail: true, + }), + }); + } + + @bindThis + public packMany( + xs: MiReversiMatching[], + me?: { id: MiUser['id'] } | null | undefined, + ) { + return Promise.all(xs.map(x => this.pack(x, me))); + } +} + diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 176978d35f..cabee95dad 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -39,6 +39,8 @@ import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js'; import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js'; import { packedAdSchema } from '@/models/json-schema/ad.js'; +import { packedReversiGameSchema } from '@/models/json-schema/reversi-game.js'; +import { packedReversiMatchingSchema } from '@/models/json-schema/reversi-matching.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -78,6 +80,8 @@ export const refs = { Signin: packedSigninSchema, RoleLite: packedRoleLiteSchema, Role: packedRoleSchema, + ReversiGame: packedReversiGameSchema, + ReversiMatching: packedReversiMatchingSchema, }; export type Packed = SchemaType; diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts new file mode 100644 index 0000000000..fc7cdb063d --- /dev/null +++ b/packages/backend/src/models/json-schema/reversi-game.ts @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedReversiGameSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + startedAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, + isStarted: { + type: 'boolean', + optional: false, nullable: false, + }, + isEnded: { + type: 'boolean', + optional: false, nullable: false, + }, + form1: { + type: 'any', + optional: false, nullable: true, + }, + form2: { + type: 'any', + optional: false, nullable: true, + }, + user1Accepted: { + type: 'boolean', + optional: false, nullable: false, + }, + user2Accepted: { + type: 'boolean', + optional: false, nullable: false, + }, + user1Id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user2Id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user1: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, + user2: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, + winnerId: { + type: 'string', + optional: false, nullable: true, + format: 'id', + }, + winner: { + type: 'object', + optional: false, nullable: true, + ref: 'User', + }, + surrendered: { + type: 'string', + optional: false, nullable: true, + format: 'id', + }, + black: { + type: 'number', + optional: false, nullable: true, + }, + bw: { + type: 'string', + optional: false, nullable: false, + }, + isLlotheo: { + type: 'boolean', + optional: false, nullable: false, + }, + canPutEverywhere: { + type: 'boolean', + optional: false, nullable: false, + }, + loopedBoard: { + type: 'boolean', + optional: false, nullable: false, + }, + logs: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'object', + optional: true, nullable: false, + properties: { + at: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + color: { + type: 'boolean', + optional: false, nullable: false, + }, + pos: { + type: 'number', + optional: false, nullable: false, + }, + }, + }, + }, + map: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/reversi-matching.ts b/packages/backend/src/models/json-schema/reversi-matching.ts new file mode 100644 index 0000000000..a98b0a1644 --- /dev/null +++ b/packages/backend/src/models/json-schema/reversi-matching.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedReversiMatchingSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + parentId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + parent: { + type: 'object', + optional: false, nullable: true, + ref: 'User', + }, + childId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + child: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, + }, +} as const; From 8ef70283fa308f390519de3396ff2361af16ab47 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 11:59:47 +0900 Subject: [PATCH 0005/1120] wip --- packages/backend/src/core/ReversiService.ts | 108 ++++++++++-------- .../src/server/api/endpoints/reversi/match.ts | 77 ++----------- 2 files changed, 75 insertions(+), 110 deletions(-) diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 64c0e25334..b6ddc725a7 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -11,6 +11,8 @@ import type { MiReversiGame, MiRole, MiRoleAssignment, + ReversiGamesRepository, + ReversiMatchingsRepository, RoleAssignmentsRepository, RolesRepository, UsersRepository, @@ -30,6 +32,7 @@ import { ModerationLogService } from '@/core/ModerationLogService.js'; import type { Packed } from '@/misc/json-schema.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { NotificationService } from '@/core/NotificationService.js'; +import { ReversiMatchingEntityService } from '@/core/entities/ReversiMatchingEntityService.js'; import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; @Injectable() @@ -51,19 +54,18 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.rolesRepository) - private rolesRepository: RolesRepository, + @Inject(DI.reversiGamesRepository) + private reversiGamesRepository: ReversiGamesRepository, - @Inject(DI.roleAssignmentsRepository) - private roleAssignmentsRepository: RoleAssignmentsRepository, + @Inject(DI.reversiMatchingsRepository) + private reversiMatchingsRepository: ReversiMatchingsRepository, private metaService: MetaService, private cacheService: CacheService, private userEntityService: UserEntityService, private globalEventService: GlobalEventService, + private reversiMatchingsEntityService: ReversiMatchingEntityService, private idService: IdService, - private moderationLogService: ModerationLogService, - private fanoutTimelineService: FanoutTimelineService, ) { } @@ -72,47 +74,63 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async getModerators(includeAdmins = true): Promise { - const ids = await this.getModeratorIds(includeAdmins); - const users = ids.length > 0 ? await this.usersRepository.findBy({ - id: In(ids), - }) : []; - return users; - } - - @bindThis - public async create(values: Partial, moderator?: MiUser): Promise { - const date = new Date(); - const created = await this.rolesRepository.insert({ - id: this.idService.gen(date.getTime()), - updatedAt: date, - lastUsedAt: date, - name: values.name, - description: values.description, - color: values.color, - iconUrl: values.iconUrl, - target: values.target, - condFormula: values.condFormula, - isPublic: values.isPublic, - isAdministrator: values.isAdministrator, - isModerator: values.isModerator, - isExplorable: values.isExplorable, - asBadge: values.asBadge, - canEditMembersByModerator: values.canEditMembersByModerator, - displayOrder: values.displayOrder, - policies: values.policies, - }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); - - this.globalEventService.publishInternalEvent('roleCreated', created); - - if (moderator) { - this.moderationLogService.log(moderator, 'createRole', { - roleId: created.id, - role: created, - }); + public async match(me: MiUser, targetUser: MiUser): Promise { + if (targetUser.id === me.id) { + throw new Error('You cannot match yourself.'); } - return created; + const exist = await this.reversiMatchingsRepository.findOneBy({ + parentId: targetUser.id, + childId: me.id, + }); + + if (exist) { + this.reversiMatchingsRepository.delete(exist.id); + + const game = await this.reversiGamesRepository.insert({ + id: this.idService.gen(), + user1Id: exist.parentId, + user2Id: me.id, + user1Accepted: false, + user2Accepted: false, + isStarted: false, + isEnded: false, + logs: [], + map: eighteight.data, + bw: 'random', + isLlotheo: false, + }).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0])); + + publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, { id: exist.parentId })); + + const other = await this.reversiMatchingsRepository.countBy({ + childId: me.id, + }); + + if (other == 0) { + publishMainStream(me.id, 'reversiNoInvites'); + } + + return game; + } else { + const child = targetUser; + + await this.reversiMatchingsRepository.delete({ + parentId: me.id, + }); + + const matching = await this.reversiMatchingsRepository.insert({ + id: this.idService.gen(), + parentId: me.id, + childId: child.id, + }).then(x => this.reversiMatchingsRepository.findOneByOrFail(x.identifiers[0])); + + const packed = await this.reversiMatchingsEntityService.pack(matching, child); + publishReversiStream(child.id, 'invited', packed); + publishMainStream(child.id, 'reversiInvited', packed); + + return null; + } } @bindThis diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts index 86d3ad6da4..850c65d5ff 100644 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -3,11 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { IdService } from '@/core/IdService.js'; -import type { ReversiGamesRepository, ReversiMatchingsRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; +import { ReversiService } from '@/core/ReversiService.js'; +import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; import { ApiError } from '../../error.js'; import { GetterService } from '../../GetterService.js'; @@ -45,75 +44,23 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.reversiGamesRepository) - private reversiGamesRepository: ReversiGamesRepository, - - @Inject(DI.reversiMatchingsRepository) - private reversiMatchingsRepository: ReversiMatchingsRepository, - private getterService: GetterService, - private idService: IdService, + private reversiService: ReversiService, + private reversiGameEntityService: ReversiGameEntityService, ) { super(meta, paramDef, async (ps, me) => { - if (ps.userId === me.id) { - throw new ApiError(meta.errors.isYourself); - } + if (ps.userId === me.id) throw new ApiError(meta.errors.isYourself); - const exist = await this.reversiMatchingsRepository.findOneBy({ - parentId: ps.userId, - childId: me.id, + const child = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; }); - if (exist) { - this.reversiMatchingsRepository.delete(exist.id); + const game = await this.reversiService.match(me, child); - const game = await this.reversiGamesRepository.insert({ - id: this.idService.gen(), - user1Id: exist.parentId, - user2Id: me.id, - user1Accepted: false, - user2Accepted: false, - isStarted: false, - isEnded: false, - logs: [], - map: eighteight.data, - bw: 'random', - isLlotheo: false, - }); + if (game == null) return; - publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, { id: exist.parentId })); - - const other = await this.reversiMatchingsRepository.countBy({ - childId: me.id, - }); - - if (other == 0) { - publishMainStream(me.id, 'reversiNoInvites'); - } - - return await ReversiGames.pack(game, me); - } else { - const child = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); - - await this.reversiMatchingsRepository.delete({ - parentId: me.id, - }); - - const matching = await this.reversiMatchingsRepository.insert({ - id: this.idService.gen(), - parentId: me.id, - childId: child.id, - }); - - const packed = await ReversiMatchings.pack(matching, child); - publishReversiStream(child.id, 'invited', packed); - publishMainStream(child.id, 'reversiInvited', packed); - - return; - } + return await this.reversiGameEntityService.pack(game, me); }); } } From 1a2540145202f63d5b6a8e396a4e68f1a4d6b49c Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 15:28:39 +0900 Subject: [PATCH 0006/1120] wip --- packages/backend/package.json | 1 + packages/backend/src/core/ReversiService.ts | 29 +- .../api/endpoints/reversi/cancel-match.ts | 38 ++ .../server/api/endpoints/reversi/show-game.ts | 52 +++ packages/frontend/package.json | 1 + .../{reversi => misskey-reversi}/package.json | 10 +- .../engine.ts => misskey-reversi/src/game.ts} | 30 +- packages/misskey-reversi/src/index.ts | 7 + .../{reversi => misskey-reversi}/src/maps.ts | 0 .../tsconfig.json | 0 pnpm-lock.yaml | 387 ++++++++++++++++-- pnpm-workspace.yaml | 1 + 12 files changed, 494 insertions(+), 62 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/reversi/cancel-match.ts create mode 100644 packages/backend/src/server/api/endpoints/reversi/show-game.ts rename packages/{reversi => misskey-reversi}/package.json (70%) rename packages/{reversi/src/engine.ts => misskey-reversi/src/game.ts} (91%) create mode 100644 packages/misskey-reversi/src/index.ts rename packages/{reversi => misskey-reversi}/src/maps.ts (100%) rename packages/{reversi => misskey-reversi}/tsconfig.json (100%) diff --git a/packages/backend/package.json b/packages/backend/package.json index ef83be0b01..f8e82c5a1c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -134,6 +134,7 @@ "microformats-parser": "2.0.2", "mime-types": "2.1.35", "misskey-js": "workspace:*", + "misskey-reversi": "workspace:*", "ms": "3.0.0-canary.1", "nanoid": "5.0.4", "nested-property": "4.0.0", diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index b6ddc725a7..ad6071d18c 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -7,32 +7,26 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import { In } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; +import * as Reversi from 'misskey-reversi'; import type { MiReversiGame, - MiRole, - MiRoleAssignment, ReversiGamesRepository, ReversiMatchingsRepository, - RoleAssignmentsRepository, - RolesRepository, UsersRepository, } from '@/models/_.js'; -import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; -import type { RoleCondFormulaValue } from '@/models/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { IdService } from '@/core/IdService.js'; -import { ModerationLogService } from '@/core/ModerationLogService.js'; import type { Packed } from '@/misc/json-schema.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { ReversiMatchingEntityService } from '@/core/entities/ReversiMatchingEntityService.js'; +import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; @Injectable() @@ -64,6 +58,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { private cacheService: CacheService, private userEntityService: UserEntityService, private globalEventService: GlobalEventService, + private reversiGameEntityService: ReversiGameEntityService, private reversiMatchingsEntityService: ReversiMatchingEntityService, private idService: IdService, ) { @@ -96,18 +91,18 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { isStarted: false, isEnded: false, logs: [], - map: eighteight.data, + map: Reversi.maps.eighteight.data, bw: 'random', isLlotheo: false, }).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0])); - publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, { id: exist.parentId })); + publishReversiStream(exist.parentId, 'matched', await this.reversiGameEntityService.pack(game, { id: exist.parentId })); const other = await this.reversiMatchingsRepository.countBy({ childId: me.id, }); - if (other == 0) { + if (other === 0) { publishMainStream(me.id, 'reversiNoInvites'); } @@ -133,6 +128,18 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } } + @bindThis + public async matchCancel(user: MiUser) { + await this.reversiMatchingsRepository.delete({ + parentId: user.id, + }); + } + + @bindThis + public async get(id: MiReversiGame['id']) { + return this.reversiGamesRepository.findOneBy({ id }); + } + @bindThis public dispose(): void { } diff --git a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts new file mode 100644 index 0000000000..cacbc6d85e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts @@ -0,0 +1,38 @@ +/* + * 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'; + +export const meta = { + requireCredential: true, + + kind: 'write:account', + + errors: { + }, + + res: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private reversiService: ReversiService, + ) { + super(meta, paramDef, async (ps, me) => { + await this.reversiService.matchCancel(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/reversi/show-game.ts b/packages/backend/src/server/api/endpoints/reversi/show-game.ts new file mode 100644 index 0000000000..b44d345b26 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/show-game.ts @@ -0,0 +1,52 @@ +/* + * 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 = { + requireCredential: false, + + errors: { + noSuchGame: { + message: 'No such game.', + code: 'NO_SUCH_GAME', + id: 'f13a03db-fae1-46c9-87f3-43c8165419e1', + }, + }, + + res: { + ref: 'ReversiGame', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + gameId: { type: 'string', format: 'misskey:id' }, + }, + required: ['gameId'], +} as const; + +@Injectable() +export default class extends Endpoint { // 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.get(ps.gameId); + + if (game == null) { + throw new ApiError(meta.errors.noSuchGame); + } + + return await this.reversiGameEntityService.pack(game, me); + }); + } +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 8c3ce30668..2cf13d9cff 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -53,6 +53,7 @@ "matter-js": "0.19.0", "mfm-js": "0.24.0", "misskey-js": "workspace:*", + "misskey-reversi": "workspace:*", "photoswipe": "5.4.3", "punycode": "2.3.1", "rollup": "4.9.1", diff --git a/packages/reversi/package.json b/packages/misskey-reversi/package.json similarity index 70% rename from packages/reversi/package.json rename to packages/misskey-reversi/package.json index 0578a341c6..8d3ca30166 100644 --- a/packages/reversi/package.json +++ b/packages/misskey-reversi/package.json @@ -1,5 +1,5 @@ { - "name": "reversi", + "name": "misskey-reversi", "version": "0.0.1", "main": "./built/index.js", "types": "./built/index.d.ts", @@ -11,10 +11,10 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@misskey-dev/eslint-plugin": "^1.0.0", - "@types/node": "20.10.5", - "@typescript-eslint/eslint-plugin": "6.14.0", - "@typescript-eslint/parser": "6.14.0", + "@misskey-dev/eslint-plugin": "1.0.0", + "@types/node": "20.11.5", + "@typescript-eslint/eslint-plugin": "6.19.0", + "@typescript-eslint/parser": "6.19.0", "eslint": "8.56.0", "typescript": "5.3.3" }, diff --git a/packages/reversi/src/engine.ts b/packages/misskey-reversi/src/game.ts similarity index 91% rename from packages/reversi/src/engine.ts rename to packages/misskey-reversi/src/game.ts index 59fef8f587..55d0b84da7 100644 --- a/packages/reversi/src/engine.ts +++ b/packages/misskey-reversi/src/game.ts @@ -31,7 +31,7 @@ export type Undo = { turn: Color | null; }; -export class ReversiGame { +export class Game { public map: MapCell[]; public mapWidth: number; public mapHeight: number; @@ -79,13 +79,13 @@ export class ReversiGame { return this.board.filter(x => x === WHITE).length; } - public transformPosToXy(pos: number): number[] { + public posToXy(pos: number): number[] { const x = pos % this.mapWidth; const y = Math.floor(pos / this.mapWidth); return [x, y]; } - public transformXyToPos(x: number, y: number): number { + public xyToPos(x: number, y: number): number { return x + (y * this.mapWidth); } @@ -136,7 +136,7 @@ export class ReversiGame { } public mapDataGet(pos: number): MapCell { - const [x, y] = this.transformPosToXy(pos); + const [x, y] = this.posToXy(pos); return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos]; } @@ -164,26 +164,26 @@ export class ReversiGame { const enemyColor = !color; const diffVectors: [number, number][] = [ - [ 0, -1], // 上 - [ +1, -1], // 右上 - [ +1, 0], // 右 - [ +1, +1], // 右下 - [ 0, +1], // 下 - [ -1, +1], // 左下 - [ -1, 0], // 左 - [ -1, -1] // 左上 + [ 0, -1], // 上 + [+1, -1], // 右上 + [+1, 0], // 右 + [+1, +1], // 右下 + [ 0, +1], // 下 + [-1, +1], // 左下 + [-1, 0], // 左 + [-1, -1] // 左上 ]; const effectsInLine = ([dx, dy]: [number, number]): number[] => { const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy]; const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列 - let [x, y] = this.transformPosToXy(initPos); + let [x, y] = this.posToXy(initPos); while (true) { [x, y] = nextPos(x, y); // 座標が指し示す位置がボード外に出たとき - if (this.opts.loopedBoard && this.transformXyToPos( + if (this.opts.loopedBoard && this.xyToPos( (x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth), (y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos) // 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ) @@ -191,7 +191,7 @@ export class ReversiGame { else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight) return []; // 挟めないことが確定 (盤面外に到達) - const pos = this.transformXyToPos(x, y); + const pos = this.xyToPos(x, y); if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達) const stone = this.board[pos]; if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達) diff --git a/packages/misskey-reversi/src/index.ts b/packages/misskey-reversi/src/index.ts new file mode 100644 index 0000000000..20ed36f208 --- /dev/null +++ b/packages/misskey-reversi/src/index.ts @@ -0,0 +1,7 @@ +import { Game } from './game.js'; + +export { + Game, +}; + +export * as maps from './maps.js'; diff --git a/packages/reversi/src/maps.ts b/packages/misskey-reversi/src/maps.ts similarity index 100% rename from packages/reversi/src/maps.ts rename to packages/misskey-reversi/src/maps.ts diff --git a/packages/reversi/tsconfig.json b/packages/misskey-reversi/tsconfig.json similarity index 100% rename from packages/reversi/tsconfig.json rename to packages/misskey-reversi/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4fc99c483e..c734efecaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -266,6 +266,9 @@ importers: misskey-js: specifier: workspace:* version: link:../misskey-js + misskey-reversi: + specifier: workspace:* + version: link:../misskey-reversi ms: specifier: 3.0.0-canary.1 version: 3.0.0-canary.1 @@ -775,6 +778,9 @@ importers: misskey-js: specifier: workspace:* version: link:../misskey-js + misskey-reversi: + specifier: workspace:* + version: link:../misskey-reversi photoswipe: specifier: 5.4.3 version: 5.4.3 @@ -1117,6 +1123,27 @@ importers: specifier: 5.3.3 version: 5.3.3 + packages/misskey-reversi: + devDependencies: + '@misskey-dev/eslint-plugin': + specifier: 1.0.0 + version: 1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.19.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) + '@types/node': + specifier: 20.11.5 + version: 20.11.5 + '@typescript-eslint/eslint-plugin': + specifier: 6.19.0 + version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: 6.19.0 + version: 6.19.0(eslint@8.56.0)(typescript@5.3.3) + eslint: + specifier: 8.56.0 + version: 8.56.0 + typescript: + specifier: 5.3.3 + version: 5.3.3 + packages/sw: dependencies: esbuild: @@ -1131,7 +1158,7 @@ importers: devDependencies: '@misskey-dev/eslint-plugin': specifier: ^1.0.0 - version: 1.0.0(@typescript-eslint/eslint-plugin@6.14.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) + version: 1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0) '@typescript-eslint/parser': specifier: 6.14.0 version: 6.14.0(eslint@8.56.0)(typescript@5.3.3) @@ -4574,7 +4601,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -4595,14 +4622,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.7.1 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.10.5) + jest-config: 29.7.0(@types/node@20.11.5) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -4637,7 +4664,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-mock: 29.7.0 dev: true @@ -4664,7 +4691,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4697,7 +4724,7 @@ packages: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.18 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 collect-v8-coverage: 1.0.1 exit: 0.1.2 @@ -4791,7 +4818,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/yargs': 16.0.5 chalk: 4.1.2 dev: true @@ -4803,7 +4830,7 @@ packages: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/yargs': 17.0.19 chalk: 4.1.2 dev: true @@ -4995,6 +5022,34 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.56.0) dev: true + /@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0): + resolution: {integrity: sha512-dh6UbcrNDVg5DD8k8Qh4ab30OPpuEYIlJCqaBV/lkIV8wNN/AfCJ2V7iTP8V8KjryM4t+sf5IqzQLQnT0mWI4A==} + peerDependencies: + '@typescript-eslint/eslint-plugin': '>= 6' + '@typescript-eslint/parser': '>= 6' + eslint: '>= 3' + eslint-plugin-import: '>= 2' + dependencies: + '@typescript-eslint/eslint-plugin': 6.19.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.14.0)(eslint@8.56.0) + dev: true + + /@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@6.19.0)(@typescript-eslint/parser@6.19.0)(eslint-plugin-import@2.29.1)(eslint@8.56.0): + resolution: {integrity: sha512-dh6UbcrNDVg5DD8k8Qh4ab30OPpuEYIlJCqaBV/lkIV8wNN/AfCJ2V7iTP8V8KjryM4t+sf5IqzQLQnT0mWI4A==} + peerDependencies: + '@typescript-eslint/eslint-plugin': '>= 6' + '@typescript-eslint/parser': '>= 6' + eslint: '>= 3' + eslint-plugin-import: '>= 2' + dependencies: + '@typescript-eslint/eslint-plugin': 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.19.0)(eslint@8.56.0) + dev: true + /@misskey-dev/sharp-read-bmp@1.1.1: resolution: {integrity: sha512-X52BQYL/I9mafypQ+wBhst+BUlYiPWnHhKGcF6ybcYSLl+zhcV0q5mezIXHozhM0Sv0A7xCdrWmR7TCNxHLrtQ==} dependencies: @@ -7995,7 +8050,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.4 - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/responselike': 1.0.0 dev: false @@ -8028,7 +8083,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/content-disposition@0.5.8: @@ -8042,7 +8097,7 @@ packages: /@types/cross-spawn@6.0.2: resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/debug@4.1.7: @@ -8100,7 +8155,7 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 dev: true @@ -8128,13 +8183,13 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/http-cache-semantics@4.0.1: @@ -8215,7 +8270,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: false /@types/lodash@4.14.191: @@ -8264,7 +8319,7 @@ packages: /@types/node-fetch@2.6.4: resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 form-data: 3.0.1 /@types/node-fetch@3.0.3: @@ -8282,6 +8337,11 @@ packages: dependencies: undici-types: 5.26.5 + /@types/node@20.11.5: + resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} + dependencies: + undici-types: 5.26.5 + /@types/node@20.9.1: resolution: {integrity: sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==} dependencies: @@ -8384,7 +8444,7 @@ packages: /@types/readdir-glob@1.1.1: resolution: {integrity: sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/rename@1.0.7: @@ -8398,7 +8458,7 @@ packages: /@types/responselike@1.0.0: resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: false /@types/sanitize-html@2.9.5: @@ -8424,7 +8484,7 @@ packages: resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} dependencies: '@types/mime': 3.0.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/serviceworker@0.0.67: @@ -8434,7 +8494,7 @@ packages: /@types/set-cookie-parser@2.4.3: resolution: {integrity: sha512-7QhnH7bi+6KAhBB+Auejz1uV9DHiopZqu7LfR/5gZZTkejJV5nYeZZpgfFoE0N8aDsXuiYpfKyfyMatCwQhyTQ==} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /@types/sharp@0.32.0: @@ -8537,7 +8597,7 @@ packages: resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} requiresBuild: true dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true optional: true @@ -8599,6 +8659,64 @@ packages: - supports-color dev: true + /@typescript-eslint/eslint-plugin@6.19.0(@typescript-eslint/parser@6.14.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.6.2 + '@typescript-eslint/parser': 6.14.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/type-utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/eslint-plugin@6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-DUCUkQNklCQYnrBSSikjVChdc84/vMPDQSgJTHBZ64G9bA9w0Crc0rd2diujKbTdp6w2J47qkeHQLoi0rpLCdg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.6.2 + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/type-utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@6.11.0(eslint@8.53.0)(typescript@5.3.3): resolution: {integrity: sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8641,6 +8759,27 @@ packages: - supports-color dev: true + /@typescript-eslint/parser@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-1DyBLG5SH7PYCd00QlroiW60YJ4rWMuUGa/JBV0iZuqi4l4IK3twKPq5ZkEebmGqRjXWVgsUzfd3+nZveewgow==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.56.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/scope-manager@6.11.0: resolution: {integrity: sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8657,6 +8796,14 @@ packages: '@typescript-eslint/visitor-keys': 6.14.0 dev: true + /@typescript-eslint/scope-manager@6.19.0: + resolution: {integrity: sha512-dO1XMhV2ehBI6QN8Ufi7I10wmUovmLU0Oru3n5LVlM2JuzB4M+dVphCPLkVpKvGij2j/pHBWuJ9piuXx+BhzxQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/visitor-keys': 6.19.0 + dev: true + /@typescript-eslint/type-utils@6.11.0(eslint@8.53.0)(typescript@5.3.3): resolution: {integrity: sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8697,6 +8844,26 @@ packages: - supports-color dev: true + /@typescript-eslint/type-utils@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-mcvS6WSWbjiSxKCwBcXtOM5pRkPQ6kcDds/juxcy/727IQr3xMEcwr/YLHW2A2+Fp5ql6khjbKBzOyjuPqGi/w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.56.0 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/types@6.11.0: resolution: {integrity: sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8707,6 +8874,11 @@ packages: engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/types@6.19.0: + resolution: {integrity: sha512-lFviGV/vYhOy3m8BJ/nAKoAyNhInTdXpftonhWle66XHAtT1ouBlkjL496b5H5hb8dWXHwtypTqgtb/DEa+j5A==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + /@typescript-eslint/typescript-estree@6.11.0(typescript@5.3.3): resolution: {integrity: sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8749,6 +8921,28 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@6.19.0(typescript@5.3.3): + resolution: {integrity: sha512-o/zefXIbbLBZ8YJ51NlkSAt2BamrK6XOmuxSR3hynMIzzyMY33KuJ9vuMdFSXW+H0tVvdF9qBPTHA91HDb4BIQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/visitor-keys': 6.19.0 + debug: 4.3.4(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.1(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/utils@6.11.0(eslint@8.53.0)(typescript@5.3.3): resolution: {integrity: sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8787,6 +8981,25 @@ packages: - typescript dev: true + /@typescript-eslint/utils@6.19.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-QR41YXySiuN++/dC9UArYOg4X86OAYP83OWTewpVx5ct1IZhjjgTLocj7QNxGhWoTqknsgpl7L+hGygCO+sdYw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.12 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.19.0 + '@typescript-eslint/types': 6.19.0 + '@typescript-eslint/typescript-estree': 6.19.0(typescript@5.3.3) + eslint: 8.56.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@6.11.0: resolution: {integrity: sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -8803,6 +9016,14 @@ packages: eslint-visitor-keys: 3.4.3 dev: true + /@typescript-eslint/visitor-keys@6.19.0: + resolution: {integrity: sha512-hZaUCORLgubBvtGpp1JEFEazcuEdfxta9j4iUwdSAr7mEsYYAp3EAUyCZk3VEEqGj6W+AV4uWyrDGtrlawAsgQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.19.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true @@ -11809,6 +12030,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.19.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + debug: 3.2.7(supports-color@8.1.1) + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.11.0)(eslint@8.53.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} @@ -11879,6 +12129,41 @@ packages: - supports-color dev: true + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.19.0)(eslint@8.56.0): + resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 6.19.0(eslint@8.56.0)(typescript@5.3.3) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7(supports-color@8.1.1) + doctrine: 2.1.0 + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.19.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-vue@9.19.2(eslint@8.56.0): resolution: {integrity: sha512-CPDqTOG2K4Ni2o4J5wixkLVNwgctKXFu6oBpVJlpNq7f38lh9I80pRTouZSJ2MAebPJlINU/KTFSXyQfBUlymA==} engines: {node: ^14.17.0 || >=16.0.0} @@ -14047,7 +14332,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 co: 4.6.0 dedent: 1.3.0 @@ -14136,6 +14421,46 @@ packages: - supports-color dev: true + /jest-config@29.7.0(@types/node@20.11.5): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.22.11 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.5 + babel-jest: 29.7.0(@babel/core@7.22.11) + chalk: 4.1.2 + ci-info: 3.7.1 + deepmerge: 4.2.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-diff@28.1.3: resolution: {integrity: sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -14191,7 +14516,7 @@ packages: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-mock: 29.7.0 jest-util: 29.7.0 dev: true @@ -14221,7 +14546,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.6 - '@types/node': 20.10.5 + '@types/node': 20.11.5 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -14282,7 +14607,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@jest/types': 27.5.1 - '@types/node': 20.10.5 + '@types/node': 20.11.5 dev: true /jest-mock@29.7.0: @@ -14345,7 +14670,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -14376,7 +14701,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 cjs-module-lexer: 1.2.2 collect-v8-coverage: 1.0.1 @@ -14428,7 +14753,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 chalk: 4.1.2 ci-info: 3.7.1 graceful-fs: 4.2.11 @@ -14453,7 +14778,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.10.5 + '@types/node': 20.11.5 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -14472,7 +14797,7 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@types/node': 20.10.5 + '@types/node': 20.11.5 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3b2ecec7fd..3a03a58253 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,3 +4,4 @@ packages: - 'packages/sw' - 'packages/misskey-js' - 'packages/misskey-js/generator' + - 'packages/misskey-reversi' From 4e1fb618b8ddca3dd1d217922acde7ee43e72d86 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 15:46:14 +0900 Subject: [PATCH 0007/1120] wip --- .../backend/src/core/GlobalEventService.ts | 55 +++++- packages/backend/src/core/ReversiService.ts | 180 ++++++++++++++++-- .../api/stream/channels/reversi-game.ts | 167 +--------------- 3 files changed, 231 insertions(+), 171 deletions(-) diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index d175f21f2f..29a97e94cd 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -18,7 +18,7 @@ import type { MiSignin } from '@/models/Signin.js'; import type { MiPage } from '@/models/Page.js'; import type { MiWebhook } from '@/models/Webhook.js'; import type { MiMeta } from '@/models/Meta.js'; -import { MiAvatarDecoration, MiRole, MiRoleAssignment } from '@/models/_.js'; +import { MiAvatarDecoration, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js'; import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; @@ -159,6 +159,41 @@ export interface AdminEventTypes { comment: string; }; } + +export interface ReversiEventTypes { + matched: { + game: Packed<'ReversiGame'>; + }; + invited: { + game: Packed<'ReversiGame'>; + }; +} + +export interface ReversiGameEventTypes { + accept: boolean; + cancelAccept: undefined; + updateSettings: { + key: string; + value: any; + }; + initForm: { + userId: MiUser['id']; + form: any; + }; + updateForm: { + id: string; + value: any; + }; + message: { + message: string; + }; + set: { + pos: number; + }; + check: { + crc32: string; + }; +} //#endregion // 辞書(interface or type)から{ type, body }ユニオンを定義 @@ -249,6 +284,14 @@ export type GlobalEvents = { name: 'notesStream'; payload: Serialized>; }; + reversi: { + name: `reversiStream:${MiUser['id']}`; + payload: EventUnionFromDictionary>; + }; + reversiGame: { + name: `reversiGameStream:${MiReversiGame['id']}`; + payload: EventUnionFromDictionary>; + }; }; // API event definitions @@ -338,4 +381,14 @@ export class GlobalEventService { public publishAdminStream(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } + + @bindThis + public publishReversiStream(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void { + this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); + } + + @bindThis + public publishReversiGameStream(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void { + this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value); + } } diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index ad6071d18c..8f9ac25fc3 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import { In } from 'typeorm'; +import * as CRC32 from 'crc-32'; import { ModuleRef } from '@nestjs/core'; import * as Reversi from 'misskey-reversi'; import type { @@ -36,15 +36,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { constructor( private moduleRef: ModuleRef, - @Inject(DI.redis) - private redisClient: Redis.Redis, - - @Inject(DI.redisForTimelines) - private redisForTimelines: Redis.Redis, - - @Inject(DI.redisForSub) - private redisForSub: Redis.Redis, - @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -54,7 +45,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { @Inject(DI.reversiMatchingsRepository) private reversiMatchingsRepository: ReversiMatchingsRepository, - private metaService: MetaService, private cacheService: CacheService, private userEntityService: UserEntityService, private globalEventService: GlobalEventService, @@ -96,7 +86,8 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { isLlotheo: false, }).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0])); - publishReversiStream(exist.parentId, 'matched', await this.reversiGameEntityService.pack(game, { id: exist.parentId })); + const packed = await this.reversiGameEntityService.pack(game, { id: exist.parentId }); + this.globalEventService.publishReversiStream(exist.parentId, 'matched', { game: packed }); const other = await this.reversiMatchingsRepository.countBy({ childId: me.id, @@ -121,7 +112,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { }).then(x => this.reversiMatchingsRepository.findOneByOrFail(x.identifiers[0])); const packed = await this.reversiMatchingsEntityService.pack(matching, child); - publishReversiStream(child.id, 'invited', packed); + this.globalEventService.publishReversiStream(child.id, 'invited', { game: packed }); publishMainStream(child.id, 'reversiInvited', packed); return null; @@ -135,6 +126,169 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { }); } + @bindThis + public async matchAccept(game: MiReversiGame, user: MiUser, isAccepted: boolean) { + if (game.isStarted) return; + + let bothAccepted = false; + + if (game.user1Id === user.id) { + await this.reversiGamesRepository.update(game.id, { + user1Accepted: isAccepted, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'changeAccepts', { + user1: isAccepted, + user2: game.user2Accepted, + }); + + if (isAccepted && game.user2Accepted) bothAccepted = true; + } else if (game.user2Id === user.id) { + await this.reversiGamesRepository.update(game.id, { + user2Accepted: isAccepted, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'changeAccepts', { + user1: game.user1Accepted, + user2: isAccepted, + }); + + if (isAccepted && game.user1Accepted) bothAccepted = true; + } else { + return; + } + + if (bothAccepted) { + // 3秒後、まだacceptされていたらゲーム開始 + setTimeout(async () => { + const freshGame = await this.reversiGamesRepository.findOneBy({ id: game.id }); + if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; + if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; + + let bw: number; + if (freshGame.bw === 'random') { + bw = Math.random() > 0.5 ? 1 : 2; + } else { + bw = parseInt(freshGame.bw, 10); + } + + function getRandomMap() { + const mapCount = Object.entries(maps).length; + const rnd = Math.floor(Math.random() * mapCount); + return Object.values(maps)[rnd].data; + } + + const map = freshGame.map != null ? freshGame.map : getRandomMap(); + + await this.reversiGamesRepository.update(game.id, { + startedAt: new Date(), + isStarted: true, + black: bw, + map: map, + }); + + //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 + const o = new Reversi.Game(map, { + isLlotheo: freshGame.isLlotheo, + canPutEverywhere: freshGame.canPutEverywhere, + loopedBoard: freshGame.loopedBoard, + }); + + if (o.isEnded) { + let winner; + if (o.winner === true) { + winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; + } else if (o.winner === false) { + winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; + } else { + winner = null; + } + + await this.reversiGamesRepository.update(game.id, { + isEnded: true, + winnerId: winner, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'ended', { + winnerId: winner, + game: await this.reversiGameEntityService.pack(game.id, user), + }); + } + //#endregion + + this.globalEventService.publishReversiGameStream(game.id, 'started', + await this.reversiGameEntityService.pack(game.id, user)); + }, 3000); + } + } + + @bindThis + public async putStoneToGame(game: MiReversiGame, user: MiUser, pos: number) { + if (!game.isStarted) return; + if (game.isEnded) return; + if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return; + + const myColor = + ((game.user1Id === user.id) && game.black === 1) || ((game.user2Id === user.id) && game.black === 2) + ? true + : false; + + const o = new Reversi.Game(game.map, { + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + }); + + // 盤面の状態を再生 + for (const log of game.logs) { + o.put(log.color, log.pos); + } + + if (o.turn !== myColor) return; + + if (!o.canPut(myColor, pos)) return; + o.put(myColor, pos); + + let winner; + if (o.isEnded) { + if (o.winner === true) { + winner = game.black === 1 ? game.user1Id : game.user2Id; + } else if (o.winner === false) { + winner = game.black === 1 ? game.user2Id : game.user1Id; + } else { + winner = null; + } + } + + const log = { + at: new Date(), + color: myColor, + pos, + }; + + const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString(); + + game.logs.push(log); + + await this.reversiGamesRepository.update(game.id, { + crc32, + isEnded: o.isEnded, + winnerId: winner, + logs: game.logs, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'set', Object.assign(log, { + next: o.turn, + })); + + if (o.isEnded) { + this.globalEventService.publishReversiGameStream(game.id, 'ended', { + winnerId: winner, + game: await this.reversiGameEntityService.pack(game.id, user), + }); + } + } + @bindThis public async get(id: MiReversiGame['id']) { return this.reversiGamesRepository.findOneBy({ id }); 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 4b900d8e89..52cf882e0b 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -9,6 +9,7 @@ import type { MiReversiGame, MiUser, ReversiGamesRepository } from '@/models/_.j import type { Packed } from '@/misc/json-schema.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { ReversiService } from '@/core/ReversiService.js'; import Channel, { type MiChannelService } from '../channel.js'; class ReversiGameChannel extends Channel { @@ -18,6 +19,7 @@ class ReversiGameChannel extends Channel { private gameId: MiReversiGame['id'] | null = null; constructor( + private reversiService: ReversiService, private reversiGamesRepository: ReversiGamesRepository, id: string, @@ -47,7 +49,7 @@ class ReversiGameChannel extends Channel { case 'initForm': this.initForm(body); break; case 'updateForm': this.updateForm(body.id, body.value); break; case 'message': this.message(body); break; - case 'set': this.set(body.pos); break; + case 'putStone': this.putStone(body.pos); break; case 'check': this.check(body.crc32); break; } } @@ -151,170 +153,18 @@ class ReversiGameChannel extends Channel { const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); if (game == null) throw new Error('game not found'); - if (game.isStarted) return; - - let bothAccepted = false; - - if (game.user1Id === this.user.id) { - await this.reversiGamesRepository.update(this.gameId!, { - user1Accepted: accept, - }); - - publishReversiGameStream(this.gameId!, 'changeAccepts', { - user1: accept, - user2: game.user2Accepted, - }); - - if (accept && game.user2Accepted) bothAccepted = true; - } else if (game.user2Id === this.user.id) { - await this.reversiGamesRepository.update(this.gameId!, { - user2Accepted: accept, - }); - - publishReversiGameStream(this.gameId!, 'changeAccepts', { - user1: game.user1Accepted, - user2: accept, - }); - - if (accept && game.user1Accepted) bothAccepted = true; - } else { - return; - } - - if (bothAccepted) { - // 3秒後、まだacceptされていたらゲーム開始 - setTimeout(async () => { - const freshGame = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); - if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; - if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; - - let bw: number; - if (freshGame.bw == 'random') { - bw = Math.random() > 0.5 ? 1 : 2; - } else { - bw = parseInt(freshGame.bw, 10); - } - - function getRandomMap() { - const mapCount = Object.entries(maps).length; - const rnd = Math.floor(Math.random() * mapCount); - return Object.values(maps)[rnd].data; - } - - const map = freshGame.map != null ? freshGame.map : getRandomMap(); - - await this.reversiGamesRepository.update(this.gameId!, { - startedAt: new Date(), - isStarted: true, - black: bw, - map: map, - }); - - //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 - const o = new ReversiGame(map, { - isLlotheo: freshGame.isLlotheo, - canPutEverywhere: freshGame.canPutEverywhere, - loopedBoard: freshGame.loopedBoard, - }); - - if (o.isEnded) { - let winner; - if (o.winner === true) { - winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; - } else if (o.winner === false) { - winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; - } else { - winner = null; - } - - await this.reversiGamesRepository.update(this.gameId!, { - isEnded: true, - winnerId: winner, - }); - - publishReversiGameStream(this.gameId!, 'ended', { - winnerId: winner, - game: await ReversiGames.pack(this.gameId!, this.user), - }); - } - //#endregion - - publishReversiGameStream(this.gameId!, 'started', - await ReversiGames.pack(this.gameId!, this.user)); - }, 3000); - } + this.reversiService.matchAccept(game, this.user, accept); } @bindThis - private async set(pos: number) { + private async putStone(pos: number) { if (this.user == null) return; + // TODO: キャッシュしたい const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); if (game == null) throw new Error('game not found'); - if (!game.isStarted) return; - if (game.isEnded) return; - if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - - const myColor = - ((game.user1Id === this.user.id) && game.black === 1) || ((game.user2Id === this.user.id) && game.black === 2) - ? true - : false; - - const o = new ReversiGame(game.map, { - isLlotheo: game.isLlotheo, - canPutEverywhere: game.canPutEverywhere, - loopedBoard: game.loopedBoard, - }); - - // 盤面の状態を再生 - for (const log of game.logs) { - o.put(log.color, log.pos); - } - - if (o.turn !== myColor) return; - - if (!o.canPut(myColor, pos)) return; - o.put(myColor, pos); - - let winner; - if (o.isEnded) { - if (o.winner === true) { - winner = game.black === 1 ? game.user1Id : game.user2Id; - } else if (o.winner === false) { - winner = game.black === 1 ? game.user2Id : game.user1Id; - } else { - winner = null; - } - } - - const log = { - at: new Date(), - color: myColor, - pos, - }; - - const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString(); - - game.logs.push(log); - - await this.reversiGamesRepository.update(this.gameId!, { - crc32, - isEnded: o.isEnded, - winnerId: winner, - logs: game.logs, - }); - - publishReversiGameStream(this.gameId!, 'set', Object.assign(log, { - next: o.turn, - })); - - if (o.isEnded) { - publishReversiGameStream(this.gameId!, 'ended', { - winnerId: winner, - game: await ReversiGames.pack(this.gameId!, this.user), - }); - } + this.reversiService.putStoneToGame(game, this.user, pos); } @bindThis @@ -345,12 +195,15 @@ export class ReversiGameChannelService implements MiChannelService { constructor( @Inject(DI.reversiGamesRepository) private reversiGamesRepository: ReversiGamesRepository, + + private reversiService: ReversiService, ) { } @bindThis public create(id: string, connection: Channel['connection']): ReversiGameChannel { return new ReversiGameChannel( + this.reversiService, this.reversiGamesRepository, id, connection, From 36450d7fac926b3685a7c4de0d612daefb41c142 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 16:02:24 +0900 Subject: [PATCH 0008/1120] wip --- packages/backend/src/core/ReversiService.ts | 4 +- .../src/server/api/stream/ChannelsService.ts | 6 ++ .../src/server/api/stream/channels/reversi.ts | 82 +++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 8f9ac25fc3..c2b611ce5e 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -197,9 +197,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { if (o.isEnded) { let winner; if (o.winner === true) { - winner = freshGame.black == 1 ? freshGame.user1Id : freshGame.user2Id; + winner = freshGame.black === 1 ? freshGame.user1Id : freshGame.user2Id; } else if (o.winner === false) { - winner = freshGame.black == 1 ? freshGame.user2Id : freshGame.user1Id; + winner = freshGame.black === 1 ? freshGame.user2Id : freshGame.user1Id; } else { winner = null; } diff --git a/packages/backend/src/server/api/stream/ChannelsService.ts b/packages/backend/src/server/api/stream/ChannelsService.ts index 3bc5380132..998429dd0a 100644 --- a/packages/backend/src/server/api/stream/ChannelsService.ts +++ b/packages/backend/src/server/api/stream/ChannelsService.ts @@ -19,6 +19,8 @@ import { AntennaChannelService } from './channels/antenna.js'; import { DriveChannelService } from './channels/drive.js'; import { HashtagChannelService } from './channels/hashtag.js'; import { RoleTimelineChannelService } from './channels/role-timeline.js'; +import { ReversiChannelService } from './channels/reversi.js'; +import { ReversiGameChannelService } from './channels/reversi-game.js'; import { type MiChannelService } from './channel.js'; @Injectable() @@ -38,6 +40,8 @@ export class ChannelsService { private serverStatsChannelService: ServerStatsChannelService, private queueStatsChannelService: QueueStatsChannelService, private adminChannelService: AdminChannelService, + private reversiChannelService: ReversiChannelService, + private reversiGameChannelService: ReversiGameChannelService, ) { } @@ -58,6 +62,8 @@ export class ChannelsService { case 'serverStats': return this.serverStatsChannelService; case 'queueStats': return this.queueStatsChannelService; case 'admin': return this.adminChannelService; + case 'reversi': return this.reversiChannelService; + case 'reversiGame': return this.reversiGameChannelService; default: throw new Error(`no such channel: ${name}`); diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts index e69de29bb2..eea9ba17c8 100644 --- a/packages/backend/src/server/api/stream/channels/reversi.ts +++ b/packages/backend/src/server/api/stream/channels/reversi.ts @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import * as CRC32 from 'crc-32'; +import type { MiReversiGame, MiUser, ReversiGamesRepository } from '@/models/_.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { ReversiService } from '@/core/ReversiService.js'; +import Channel, { type MiChannelService } from '../channel.js'; + +class ReversiChannel extends Channel { + public readonly chName = 'reversi'; + public static shouldShare = true; + public static requireCredential = true as const; + public static kind = 'read:account'; + + constructor( + private reversiService: ReversiService, + private reversiGamesRepository: ReversiGamesRepository, + + id: string, + connection: Channel['connection'], + ) { + super(id, connection); + } + + @bindThis + public async init(params: any) { + this.subscriber.on(`reversiStream:${this.user.id}`, this.send); + } + + @bindThis + public async async onMessage(type: string, body: any) { + switch (type) { + case 'ping': { + if (body.id == null) return; + const matching = await ReversiMatchings.findOne({ + parentId: this.user!.id, + childId: body.id, + }); + if (matching == null) return; + publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, { id: matching.childId })); + break; + } + } + } + + @bindThis + public dispose() { + // Unsubscribe events + this.subscriber.off(`reversiStream:${this.user.id}`, this.send); + } +} + +@Injectable() +export class ReversiChannelService implements MiChannelService { + public readonly shouldShare = ReversiChannel.shouldShare; + public readonly requireCredential = ReversiChannel.requireCredential; + public readonly kind = ReversiChannel.kind; + + constructor( + @Inject(DI.reversiGamesRepository) + private reversiGamesRepository: ReversiGamesRepository, + + private reversiService: ReversiService, + ) { + } + + @bindThis + public create(id: string, connection: Channel['connection']): ReversiChannel { + return new ReversiChannel( + this.reversiService, + this.reversiGamesRepository, + id, + connection, + ); + } +} From 768d0bdc00929ea7d43808bcfe8f6cbfbfa9288f Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 16:10:28 +0900 Subject: [PATCH 0009/1120] wip --- .../backend/src/core/GlobalEventService.ts | 19 +++-- packages/backend/src/core/ReversiService.ts | 28 ++++++- .../api/stream/channels/reversi-game.ts | 76 +------------------ 3 files changed, 37 insertions(+), 86 deletions(-) diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 29a97e94cd..560c5e45d1 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -172,18 +172,14 @@ export interface ReversiEventTypes { export interface ReversiGameEventTypes { accept: boolean; cancelAccept: undefined; + changeAcceptingStates: { + user1: boolean; + user2: boolean; + }; updateSettings: { key: string; value: any; }; - initForm: { - userId: MiUser['id']; - form: any; - }; - updateForm: { - id: string; - value: any; - }; message: { message: string; }; @@ -193,6 +189,13 @@ export interface ReversiGameEventTypes { check: { crc32: string; }; + started: { + game: Packed<'ReversiGame'>; + }; + ended: { + winnerId: MiUser['id'] | null; + game: Packed<'ReversiGame'>; + }; } //#endregion diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index c2b611ce5e..126b211066 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -137,7 +137,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { user1Accepted: isAccepted, }); - this.globalEventService.publishReversiGameStream(game.id, 'changeAccepts', { + this.globalEventService.publishReversiGameStream(game.id, 'changeAcceptingStates', { user1: isAccepted, user2: game.user2Accepted, }); @@ -148,7 +148,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { user2Accepted: isAccepted, }); - this.globalEventService.publishReversiGameStream(game.id, 'changeAccepts', { + this.globalEventService.publishReversiGameStream(game.id, 'changeAcceptingStates', { user1: game.user1Accepted, user2: isAccepted, }); @@ -216,12 +216,32 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } //#endregion - this.globalEventService.publishReversiGameStream(game.id, 'started', - await this.reversiGameEntityService.pack(game.id, user)); + this.globalEventService.publishReversiGameStream(game.id, 'started', { + game: await this.reversiGameEntityService.pack(game.id, user), + }); }, 3000); } } + @bindThis + public async updateSettings(game: MiReversiGame, user: MiUser, key: string, value: any) { + if (game.isStarted) return; + if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return; + if ((game.user1Id === user.id) && game.user1Accepted) return; + if ((game.user2Id === user.id) && game.user2Accepted) return; + + if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return; + + await this.reversiGamesRepository.update(game.id, { + [key]: value, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', { + key: key, + value: value, + }); + } + @bindThis public async putStoneToGame(game: MiReversiGame, user: MiUser, pos: number) { if (!game.isStarted) return; 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 52cf882e0b..be679c401b 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -46,8 +46,6 @@ class ReversiGameChannel extends Channel { case 'accept': this.accept(true); break; case 'cancelAccept': this.accept(false); break; case 'updateSettings': this.updateSettings(body.key, body.value); break; - case 'initForm': this.initForm(body); break; - case 'updateForm': this.updateForm(body.id, body.value); break; case 'message': this.message(body); break; case 'putStone': this.putStone(body.pos); break; case 'check': this.check(body.crc32); break; @@ -58,81 +56,11 @@ class ReversiGameChannel extends Channel { private async updateSettings(key: string, value: any) { if (this.user == null) return; + // TODO: キャッシュしたい const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); if (game == null) throw new Error('game not found'); - if (game.isStarted) return; - if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - if ((game.user1Id === this.user.id) && game.user1Accepted) return; - if ((game.user2Id === this.user.id) && game.user2Accepted) return; - - if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return; - - await this.reversiGamesRepository.update(this.gameId!, { - [key]: value, - }); - - publishReversiGameStream(this.gameId!, 'updateSettings', { - key: key, - value: value, - }); - } - - @bindThis - private async initForm(form: any) { - if (this.user == null) return; - - const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); - if (game == null) throw new Error('game not found'); - - if (game.isStarted) return; - if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - - const set = game.user1Id === this.user.id ? { - form1: form, - } : { - form2: form, - }; - - await this.reversiGamesRepository.update(this.gameId!, set); - - publishReversiGameStream(this.gameId!, 'initForm', { - userId: this.user.id, - form, - }); - } - - @bindThis - private async updateForm(id: string, value: any) { - if (this.user == null) return; - - const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); - if (game == null) throw new Error('game not found'); - - if (game.isStarted) return; - if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - - const form = game.user1Id === this.user.id ? game.form2 : game.form1; - - const item = form.find((i: any) => i.id == id); - - if (item == null) return; - - item.value = value; - - const set = game.user1Id === this.user.id ? { - form2: form, - } : { - form1: form, - }; - - await this.reversiGamesRepository.update(this.gameId!, set); - - publishReversiGameStream(this.gameId!, 'updateForm', { - userId: this.user.id, - id, - value, - }); + this.reversiService.updateSettings(game, this.user, key, value); } @bindThis From a78013d0e5aabe218b0615abc8c446c40f2b53ae Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 17:38:24 +0900 Subject: [PATCH 0010/1120] wip --- packages/backend/src/core/ReversiService.ts | 5 --- .../src/server/api/stream/channels/reversi.ts | 36 ++----------------- 2 files changed, 3 insertions(+), 38 deletions(-) diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 126b211066..adaf3fbaff 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -93,10 +93,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { childId: me.id, }); - if (other === 0) { - publishMainStream(me.id, 'reversiNoInvites'); - } - return game; } else { const child = targetUser; @@ -113,7 +109,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const packed = await this.reversiMatchingsEntityService.pack(matching, child); this.globalEventService.publishReversiStream(child.id, 'invited', { game: packed }); - publishMainStream(child.id, 'reversiInvited', packed); return null; } diff --git a/packages/backend/src/server/api/stream/channels/reversi.ts b/packages/backend/src/server/api/stream/channels/reversi.ts index eea9ba17c8..cb4b1b8d5a 100644 --- a/packages/backend/src/server/api/stream/channels/reversi.ts +++ b/packages/backend/src/server/api/stream/channels/reversi.ts @@ -3,13 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable } from '@nestjs/common'; -import * as CRC32 from 'crc-32'; -import type { MiReversiGame, MiUser, ReversiGamesRepository } from '@/models/_.js'; -import type { Packed } from '@/misc/json-schema.js'; -import { DI } from '@/di-symbols.js'; +import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; -import { ReversiService } from '@/core/ReversiService.js'; import Channel, { type MiChannelService } from '../channel.js'; class ReversiChannel extends Channel { @@ -19,9 +14,6 @@ class ReversiChannel extends Channel { public static kind = 'read:account'; constructor( - private reversiService: ReversiService, - private reversiGamesRepository: ReversiGamesRepository, - id: string, connection: Channel['connection'], ) { @@ -30,29 +22,13 @@ class ReversiChannel extends Channel { @bindThis public async init(params: any) { - this.subscriber.on(`reversiStream:${this.user.id}`, this.send); - } - - @bindThis - public async async onMessage(type: string, body: any) { - switch (type) { - case 'ping': { - if (body.id == null) return; - const matching = await ReversiMatchings.findOne({ - parentId: this.user!.id, - childId: body.id, - }); - if (matching == null) return; - publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, { id: matching.childId })); - break; - } - } + this.subscriber.on(`reversiStream:${this.user!.id}`, this.send); } @bindThis public dispose() { // Unsubscribe events - this.subscriber.off(`reversiStream:${this.user.id}`, this.send); + this.subscriber.off(`reversiStream:${this.user!.id}`, this.send); } } @@ -63,18 +39,12 @@ export class ReversiChannelService implements MiChannelService { public readonly kind = ReversiChannel.kind; constructor( - @Inject(DI.reversiGamesRepository) - private reversiGamesRepository: ReversiGamesRepository, - - private reversiService: ReversiService, ) { } @bindThis public create(id: string, connection: Channel['connection']): ReversiChannel { return new ReversiChannel( - this.reversiService, - this.reversiGamesRepository, id, connection, ); From 7794e7ae2fc3c3f085523cd45df4c1052296ec62 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 17:43:59 +0900 Subject: [PATCH 0011/1120] wip --- .../backend/src/core/GlobalEventService.ts | 7 ++--- .../api/stream/channels/reversi-game.ts | 27 +++++++------------ 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 560c5e45d1..9e44b2c10f 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -180,13 +180,10 @@ export interface ReversiGameEventTypes { key: string; value: any; }; - message: { - message: string; - }; - set: { + putStone: { pos: number; }; - check: { + syncState: { crc32: string; }; started: { 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 be679c401b..4234da2624 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -4,12 +4,11 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import * as CRC32 from 'crc-32'; -import type { MiReversiGame, MiUser, ReversiGamesRepository } from '@/models/_.js'; -import type { Packed } from '@/misc/json-schema.js'; +import type { MiReversiGame, ReversiGamesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { ReversiService } from '@/core/ReversiService.js'; +import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; import Channel, { type MiChannelService } from '../channel.js'; class ReversiGameChannel extends Channel { @@ -21,6 +20,7 @@ class ReversiGameChannel extends Channel { constructor( private reversiService: ReversiService, private reversiGamesRepository: ReversiGamesRepository, + private reversiGameEntityService: ReversiGameEntityService, id: string, connection: Channel['connection'], @@ -46,9 +46,8 @@ class ReversiGameChannel extends Channel { case 'accept': this.accept(true); break; case 'cancelAccept': this.accept(false); break; case 'updateSettings': this.updateSettings(body.key, body.value); break; - case 'message': this.message(body); break; case 'putStone': this.putStone(body.pos); break; - case 'check': this.check(body.crc32); break; + case 'syncState': this.syncState(body.crc32); break; } } @@ -63,17 +62,6 @@ class ReversiGameChannel extends Channel { this.reversiService.updateSettings(game, this.user, key, value); } - @bindThis - private async message(message: any) { - if (this.user == null) return; - - message.id = Math.random(); - publishReversiGameStream(this.gameId!, 'message', { - userId: this.user.id, - message, - }); - } - @bindThis private async accept(accept: boolean) { if (this.user == null) return; @@ -96,14 +84,15 @@ class ReversiGameChannel extends Channel { } @bindThis - private async check(crc32: string | number) { + private async syncState(crc32: string | number) { + // TODO: キャッシュしたい const game = await this.reversiGamesRepository.findOneBy({ id: this.gameId! }); if (game == null) throw new Error('game not found'); if (!game.isStarted) return; if (crc32.toString() !== game.crc32) { - this.send('rescue', await ReversiGames.pack(game, this.user)); + this.send('rescue', await this.reversiGameEntityService.pack(game, this.user)); } } @@ -125,6 +114,7 @@ export class ReversiGameChannelService implements MiChannelService { private reversiGamesRepository: ReversiGamesRepository, private reversiService: ReversiService, + private reversiGameEntityService: ReversiGameEntityService, ) { } @@ -133,6 +123,7 @@ export class ReversiGameChannelService implements MiChannelService { return new ReversiGameChannel( this.reversiService, this.reversiGamesRepository, + this.reversiGameEntityService, id, connection, ); From fbad40bb9bd111bb1f7256d8156f4b70450860b7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 17:50:24 +0900 Subject: [PATCH 0012/1120] wip --- packages/backend/src/core/GlobalEventService.ts | 2 +- packages/backend/src/core/ReversiService.ts | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 9e44b2c10f..b9b44b45b1 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -165,7 +165,7 @@ export interface ReversiEventTypes { game: Packed<'ReversiGame'>; }; invited: { - game: Packed<'ReversiGame'>; + game: Packed<'ReversiMatching'>; }; } diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index adaf3fbaff..8542da68f9 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -89,10 +89,6 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { const packed = await this.reversiGameEntityService.pack(game, { id: exist.parentId }); this.globalEventService.publishReversiStream(exist.parentId, 'matched', { game: packed }); - const other = await this.reversiMatchingsRepository.countBy({ - childId: me.id, - }); - return game; } else { const child = targetUser; @@ -168,9 +164,9 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } function getRandomMap() { - const mapCount = Object.entries(maps).length; + const mapCount = Object.entries(Reversi.maps).length; const rnd = Math.floor(Math.random() * mapCount); - return Object.values(maps)[rnd].data; + return Object.values(Reversi.maps)[rnd].data; } const map = freshGame.map != null ? freshGame.map : getRandomMap(); From 7ca93893cae580aba29638a99ea14768a0c3bd52 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 18:16:01 +0900 Subject: [PATCH 0013/1120] wip --- .../frontend/src/pages/drop-and-fusion.vue | 2 +- packages/frontend/src/pages/reversi/game.vue | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/src/pages/reversi/game.vue diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue index dd3b189c9d..beb2e714e0 100644 --- a/packages/frontend/src/pages/drop-and-fusion.vue +++ b/packages/frontend/src/pages/drop-and-fusion.vue @@ -123,7 +123,7 @@ function onGameEnd() { definePageMetadata({ title: i18n.ts.bubbleGame, - icon: 'ti ti-apple', + icon: 'ti ti-device-gamepad', }); diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue new file mode 100644 index 0000000000..c0f4668b7b --- /dev/null +++ b/packages/frontend/src/pages/reversi/game.vue @@ -0,0 +1,68 @@ + + + + + From 58f4d5d790b421920bfa14581bde3eea22787fe3 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 20:03:15 +0900 Subject: [PATCH 0014/1120] wip --- packages/backend/src/core/ReversiService.ts | 19 + .../backend/src/server/api/EndpointsModule.ts | 24 + packages/backend/src/server/api/endpoints.ts | 12 + .../src/server/api/endpoints/reversi/games.ts | 61 +++ .../api/endpoints/reversi/invitations.ts | 48 ++ .../server/api/endpoints/reversi/show-game.ts | 2 + .../server/api/endpoints/reversi/surrender.ts | 68 +++ packages/misskey-js/etc/misskey-js.api.md | 50 ++- .../misskey-js/src/autogen/apiClientJSDoc.ts | 68 ++- packages/misskey-js/src/autogen/endpoint.ts | 18 +- packages/misskey-js/src/autogen/entities.ts | 12 +- packages/misskey-js/src/autogen/models.ts | 4 +- packages/misskey-js/src/autogen/types.ts | 425 +++++++++++++++++- 13 files changed, 805 insertions(+), 6 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/reversi/games.ts create mode 100644 packages/backend/src/server/api/endpoints/reversi/invitations.ts create mode 100644 packages/backend/src/server/api/endpoints/reversi/surrender.ts diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 8542da68f9..0f264a2ed3 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -300,6 +300,25 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } } + @bindThis + public async surrender(game: MiReversiGame, user: MiUser) { + if (game.isEnded) return; + if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return; + + const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id; + + await this.reversiGamesRepository.update(game.id, { + surrendered: user.id, + isEnded: true, + winnerId: winnerId, + }); + + this.globalEventService.publishReversiGameStream(game.id, 'ended', { + winnerId: winnerId, + game: await this.reversiGameEntityService.pack(game.id, user), + }); + } + @bindThis public async get(id: MiReversiGame['id']) { return this.reversiGamesRepository.findOneBy({ id }); diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 781332d349..df69ce2385 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -366,6 +366,12 @@ import * as ep___fetchExternalResources from './endpoints/fetch-external-resourc import * as ep___retention from './endpoints/retention.js'; import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js'; import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js'; +import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js'; +import * as ep___reversi_games from './endpoints/reversi/games.js'; +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 { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; @@ -730,6 +736,12 @@ const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resource const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default }; const $bubbleGame_register: Provider = { provide: 'ep:bubble-game/register', useClass: ep___bubbleGame_register.default }; const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useClass: ep___bubbleGame_ranking.default }; +const $reversi_cancelMatch: Provider = { provide: 'ep:reversi/cancel-match', useClass: ep___reversi_cancelMatch.default }; +const $reversi_games: Provider = { provide: 'ep:reversi/games', useClass: ep___reversi_games.default }; +const $reversi_match: Provider = { provide: 'ep:reversi/match', useClass: ep___reversi_match.default }; +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 }; @Module({ imports: [ @@ -1098,6 +1110,12 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $retention, $bubbleGame_register, $bubbleGame_ranking, + $reversi_cancelMatch, + $reversi_games, + $reversi_match, + $reversi_invitations, + $reversi_showGame, + $reversi_surrender, ], exports: [ $admin_meta, @@ -1457,6 +1475,12 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl $retention, $bubbleGame_register, $bubbleGame_ranking, + $reversi_cancelMatch, + $reversi_games, + $reversi_match, + $reversi_invitations, + $reversi_showGame, + $reversi_surrender, ], }) export class EndpointsModule {} diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index f17db41a5d..0f2c8cb754 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -367,6 +367,12 @@ import * as ep___fetchExternalResources from './endpoints/fetch-external-resourc import * as ep___retention from './endpoints/retention.js'; import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js'; import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js'; +import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js'; +import * as ep___reversi_games from './endpoints/reversi/games.js'; +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'; const eps = [ ['admin/meta', ep___admin_meta], @@ -729,6 +735,12 @@ const eps = [ ['retention', ep___retention], ['bubble-game/register', ep___bubbleGame_register], ['bubble-game/ranking', ep___bubbleGame_ranking], + ['reversi/cancel-match', ep___reversi_cancelMatch], + ['reversi/games', ep___reversi_games], + ['reversi/match', ep___reversi_match], + ['reversi/invitations', ep___reversi_invitations], + ['reversi/show-game', ep___reversi_showGame], + ['reversi/surrender', ep___reversi_surrender], ]; interface IEndpointMetaBase { diff --git a/packages/backend/src/server/api/endpoints/reversi/games.ts b/packages/backend/src/server/api/endpoints/reversi/games.ts new file mode 100644 index 0000000000..420e80ccbe --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/games.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; +import { DI } from '@/di-symbols.js'; +import type { ReversiGamesRepository } from '@/models/_.js'; +import { QueryService } from '@/core/QueryService.js'; + +export const meta = { + requireCredential: false, + + res: { + type: 'array', + optional: false, nullable: false, + items: { ref: 'ReversiGame' }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + my: { type: 'boolean', default: false }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.reversiGamesRepository) + private reversiGamesRepository: ReversiGamesRepository, + + private reversiGameEntityService: ReversiGameEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId) + .andWhere('game.isStarted = TRUE'); + + if (ps.my && me) { + query.andWhere(new Brackets(qb => { + qb + .where('game.user1Id = :userId', { userId: me.id }) + .orWhere('game.user2Id = :userId', { userId: me.id }); + })); + } + + const games = await query.take(ps.limit).getMany(); + + return await this.reversiGameEntityService.packMany(games, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/reversi/invitations.ts b/packages/backend/src/server/api/endpoints/reversi/invitations.ts new file mode 100644 index 0000000000..da6cf66991 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/invitations.ts @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ReversiMatchingEntityService } from '@/core/entities/ReversiMatchingEntityService.js'; +import type { ReversiMatchingsRepository } from '@/models/_.js'; + +export const meta = { + requireCredential: true, + + kind: 'read:account', + + res: { + type: 'array', + optional: false, nullable: false, + items: { ref: 'ReversiMatching' }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.reversiMatchingsRepository) + private reversiMatchingsRepository: ReversiMatchingsRepository, + + private reversiMatchingEntityService: ReversiMatchingEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const invitations = await this.reversiMatchingsRepository.findBy({ + childId: me.id, + }); + + return await this.reversiMatchingEntityService.packMany(invitations, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/reversi/show-game.ts b/packages/backend/src/server/api/endpoints/reversi/show-game.ts index b44d345b26..e036dd6c76 100644 --- a/packages/backend/src/server/api/endpoints/reversi/show-game.ts +++ b/packages/backend/src/server/api/endpoints/reversi/show-game.ts @@ -21,6 +21,8 @@ export const meta = { }, res: { + type: 'object', + optional: false, nullable: false, ref: 'ReversiGame', }, } as const; diff --git a/packages/backend/src/server/api/endpoints/reversi/surrender.ts b/packages/backend/src/server/api/endpoints/reversi/surrender.ts new file mode 100644 index 0000000000..c47d36be33 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/reversi/surrender.ts @@ -0,0 +1,68 @@ +/* + * 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 { ApiError } from '../../error.js'; + +export const meta = { + requireCredential: true, + + kind: 'write:account', + + errors: { + noSuchGame: { + message: 'No such game.', + code: 'NO_SUCH_GAME', + id: 'ace0b11f-e0a6-4076-a30d-e8284c81b2df', + }, + + alreadyEnded: { + message: 'That game has already ended.', + code: 'ALREADY_ENDED', + id: '6c2ad4a6-cbf1-4a5b-b187-b772826cfc6d', + }, + + accessDenied: { + message: 'Access denied.', + code: 'ACCESS_DENIED', + id: '6e04164b-a992-4c93-8489-2123069973e1', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + gameId: { type: 'string', format: 'misskey:id' }, + }, + required: ['gameId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private reversiService: ReversiService, + ) { + super(meta, paramDef, async (ps, me) => { + const game = await this.reversiService.get(ps.gameId); + + if (game == null) { + throw new ApiError(meta.errors.noSuchGame); + } + + if (game.isEnded) { + throw new ApiError(meta.errors.alreadyEnded); + } + + if ((game.user1Id !== me.id) && (game.user2Id !== me.id)) { + throw new ApiError(meta.errors.accessDenied); + } + + await this.reversiService.surrender(game, me); + }); + } +} diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index f955cc5cc1..a20d77b8c2 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1623,6 +1623,16 @@ declare namespace entities { BubbleGameRegisterResponse, BubbleGameRankingRequest, BubbleGameRankingResponse, + ReversiCancelMatchResponse, + ReversiGamesRequest, + ReversiGamesResponse, + ReversiMatchRequest, + ReversiMatchResponse, + ReversiInvitationsRequest, + ReversiInvitationsResponse, + ReversiShowGameRequest, + ReversiShowGameResponse, + ReversiSurrenderRequest, Error_2 as Error, UserLite, UserDetailedNotMeOnly, @@ -1659,7 +1669,9 @@ declare namespace entities { Flash, Signin, RoleLite, - Role + Role, + ReversiGame, + ReversiMatching } } export { entities } @@ -2596,6 +2608,42 @@ type ResetPasswordRequest = operations['reset-password']['requestBody']['content // @public (undocumented) type RetentionResponse = operations['retention']['responses']['200']['content']['application/json']; +// @public (undocumented) +type ReversiCancelMatchResponse = operations['reversi/cancel-match']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiGame = components['schemas']['ReversiGame']; + +// @public (undocumented) +type ReversiGamesRequest = operations['reversi/games']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiGamesResponse = operations['reversi/games']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiInvitationsRequest = operations['reversi/invitations']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiInvitationsResponse = operations['reversi/invitations']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiMatching = components['schemas']['ReversiMatching']; + +// @public (undocumented) +type ReversiMatchRequest = operations['reversi/match']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiMatchResponse = operations['reversi/match']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiShowGameRequest = operations['reversi/show-game']['requestBody']['content']['application/json']; + +// @public (undocumented) +type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type ReversiSurrenderRequest = operations['reversi/surrender']['requestBody']['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 b60f449a71..5f61a719bc 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-13T04:31:38.782Z + * generatedAt: 2024-01-18T11:02:35.535Z */ import type { SwitchCaseResponseType } from '../api.js'; @@ -4007,5 +4007,71 @@ declare module '../api.js' { params: P, credential?: string | null, ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *No* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; } } diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index dc591a7046..53bfc84e22 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-13T04:31:38.778Z + * generatedAt: 2024-01-18T11:02:35.533Z */ import type { @@ -544,6 +544,16 @@ import type { BubbleGameRegisterResponse, BubbleGameRankingRequest, BubbleGameRankingResponse, + ReversiCancelMatchResponse, + ReversiGamesRequest, + ReversiGamesResponse, + ReversiMatchRequest, + ReversiMatchResponse, + ReversiInvitationsRequest, + ReversiInvitationsResponse, + ReversiShowGameRequest, + ReversiShowGameResponse, + ReversiSurrenderRequest, } from './entities.js'; export type Endpoints = { @@ -907,4 +917,10 @@ export type Endpoints = { 'retention': { req: EmptyRequest; res: RetentionResponse }; 'bubble-game/register': { req: BubbleGameRegisterRequest; res: BubbleGameRegisterResponse }; 'bubble-game/ranking': { req: BubbleGameRankingRequest; res: BubbleGameRankingResponse }; + 'reversi/cancel-match': { req: EmptyRequest; res: ReversiCancelMatchResponse }; + 'reversi/games': { req: ReversiGamesRequest; res: ReversiGamesResponse }; + 'reversi/match': { req: ReversiMatchRequest; res: ReversiMatchResponse }; + 'reversi/invitations': { req: ReversiInvitationsRequest; res: ReversiInvitationsResponse }; + 'reversi/show-game': { req: ReversiShowGameRequest; res: ReversiShowGameResponse }; + 'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse }; } diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index dfe24ce0d8..0ac45663ab 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-13T04:31:38.775Z + * generatedAt: 2024-01-18T11:02:35.532Z */ import { operations } from './types.js'; @@ -546,3 +546,13 @@ export type BubbleGameRegisterRequest = operations['bubble-game/register']['requ export type BubbleGameRegisterResponse = operations['bubble-game/register']['responses']['200']['content']['application/json']; export type BubbleGameRankingRequest = operations['bubble-game/ranking']['requestBody']['content']['application/json']; export type BubbleGameRankingResponse = operations['bubble-game/ranking']['responses']['200']['content']['application/json']; +export type ReversiCancelMatchResponse = operations['reversi/cancel-match']['responses']['200']['content']['application/json']; +export type ReversiGamesRequest = operations['reversi/games']['requestBody']['content']['application/json']; +export type ReversiGamesResponse = operations['reversi/games']['responses']['200']['content']['application/json']; +export type ReversiMatchRequest = operations['reversi/match']['requestBody']['content']['application/json']; +export type ReversiMatchResponse = operations['reversi/match']['responses']['200']['content']['application/json']; +export type ReversiInvitationsRequest = operations['reversi/invitations']['requestBody']['content']['application/json']; +export type ReversiInvitationsResponse = operations['reversi/invitations']['responses']['200']['content']['application/json']; +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']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 5c6bebf2fd..217733874c 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-13T04:31:38.773Z + * generatedAt: 2024-01-18T11:02:35.530Z */ import { components } from './types.js'; @@ -41,3 +41,5 @@ export type Flash = components['schemas']['Flash']; export type Signin = components['schemas']['Signin']; export type RoleLite = components['schemas']['RoleLite']; export type Role = components['schemas']['Role']; +export type ReversiGame = components['schemas']['ReversiGame']; +export type ReversiMatching = components['schemas']['ReversiMatching']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 76e2b5309c..87e10a58e6 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3,7 +3,7 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-13T04:31:38.633Z + * generatedAt: 2024-01-18T11:02:35.451Z */ /** @@ -3472,6 +3472,60 @@ export type paths = { */ post: operations['bubble-game/ranking']; }; + '/reversi/cancel-match': { + /** + * reversi/cancel-match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['reversi/cancel-match']; + }; + '/reversi/games': { + /** + * reversi/games + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['reversi/games']; + }; + '/reversi/match': { + /** + * reversi/match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['reversi/match']; + }; + '/reversi/invitations': { + /** + * reversi/invitations + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + post: operations['reversi/invitations']; + }; + '/reversi/show-game': { + /** + * reversi/show-game + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['reversi/show-game']; + }; + '/reversi/surrender': { + /** + * reversi/surrender + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + post: operations['reversi/surrender']; + }; }; export type webhooks = Record; @@ -4404,6 +4458,55 @@ export type components = { }; usersCount: number; }); + ReversiGame: { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + startedAt: string | null; + isStarted: boolean; + isEnded: boolean; + form1: Record | null; + form2: Record | null; + user1Accepted: boolean; + user2Accepted: boolean; + /** Format: id */ + user1Id: string; + /** Format: id */ + user2Id: string; + user1: components['schemas']['User']; + user2: components['schemas']['User']; + /** Format: id */ + winnerId: string | null; + winner: components['schemas']['User'] | null; + /** Format: id */ + surrendered: string | null; + black: number | null; + bw: string; + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; + logs?: { + /** Format: date-time */ + at: string; + color: boolean; + pos: number; + }[]; + map?: string[]; + }; + ReversiMatching: { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: id */ + parentId: string; + parent: components['schemas']['User'] | null; + /** Format: id */ + childId: string; + child: components['schemas']['User']; + }; }; responses: never; parameters: never; @@ -25542,5 +25645,325 @@ export type operations = { }; }; }; + /** + * reversi/cancel-match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'reversi/cancel-match': { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': unknown; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * reversi/games + * @description No description provided. + * + * **Credential required**: *No* + */ + 'reversi/games': { + requestBody: { + content: { + 'application/json': { + /** @default 10 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + /** @default false */ + my?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['ReversiGame'][]; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * reversi/match + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'reversi/match': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': unknown; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * reversi/invitations + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:account* + */ + 'reversi/invitations': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['ReversiMatching'][]; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * reversi/show-game + * @description No description provided. + * + * **Credential required**: *No* + */ + 'reversi/show-game': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + gameId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['ReversiGame']; + }; + }; + /** @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']; + }; + }; + }; + }; + /** + * reversi/surrender + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:account* + */ + 'reversi/surrender': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + gameId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + content: never; + }; + /** @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']; + }; + }; + }; + }; }; From 1259fabd7f8c1851ca1fc8cd37bf8eecc80a3798 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 18 Jan 2024 20:59:21 +0900 Subject: [PATCH 0015/1120] wip --- locales/index.d.ts | 33 ++ locales/ja-JP.yml | 33 ++ .../backend/src/core/GlobalEventService.ts | 15 +- packages/backend/src/core/ReversiService.ts | 12 +- .../core/entities/ReversiGameEntityService.ts | 66 ++- packages/backend/src/misc/json-schema.ts | 5 +- .../src/models/json-schema/reversi-game.ts | 108 +++- .../src/server/api/endpoints/reversi/games.ts | 4 +- .../src/server/api/endpoints/reversi/match.ts | 2 +- .../server/api/endpoints/reversi/show-game.ts | 4 +- .../api/stream/channels/reversi-game.ts | 2 +- .../frontend/src/pages/reversi/game.board.vue | 534 ++++++++++++++++++ .../src/pages/reversi/game.setting.vue | 185 ++++++ packages/frontend/src/pages/reversi/game.vue | 4 +- packages/frontend/src/pages/reversi/index.vue | 284 ++++++++++ packages/misskey-js/etc/misskey-js.api.md | 8 +- .../misskey-js/src/autogen/apiClientJSDoc.ts | 2 +- packages/misskey-js/src/autogen/endpoint.ts | 2 +- packages/misskey-js/src/autogen/entities.ts | 2 +- packages/misskey-js/src/autogen/models.ts | 5 +- packages/misskey-js/src/autogen/types.ts | 42 +- 21 files changed, 1297 insertions(+), 55 deletions(-) create mode 100644 packages/frontend/src/pages/reversi/game.board.vue create mode 100644 packages/frontend/src/pages/reversi/game.setting.vue create mode 100644 packages/frontend/src/pages/reversi/index.vue diff --git a/locales/index.d.ts b/locales/index.d.ts index 71134544d9..a827925518 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2625,6 +2625,39 @@ export interface Locale { "description": string; }; }; + "_reversi": { + "reversi": string; + "gameSettings": string; + "chooseBoard": string; + "blackOrWhite": string; + "blackIs": string; + "rules": string; + "thisGameIsStartedSoon": string; + "waitingForOther": string; + "waitingForMe": string; + "waitingBoth": string; + "ready": string; + "cancelReady": string; + "opponentTurn": string; + "myTurn": string; + "turnOf": string; + "pastTurnOf": string; + "surrender": string; + "surrendered": string; + "drawn": string; + "won": string; + "black": string; + "white": string; + "total": string; + "turnCount": string; + "myGames": string; + "allGames": string; + "ended": string; + "playing": string; + "isLlotheo": string; + "loopedMap": string; + "canPutEverywhere": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 743a3ca38e..f805cbf107 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2505,3 +2505,36 @@ _dataSaver: _code: title: "コードハイライト" description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。" + +_reversi: + reversi: "リバーシ" + gameSettings: "対局の設定" + chooseBoard: "ボードを選択" + blackOrWhite: "先行/後攻" + blackIs: "{name}が黒(先行)" + rules: "ルール" + thisGameIsStartedSoon: "対局はまもなく開始されます" + waitingForOther: "相手の準備が完了するのを待っています" + waitingForMe: "あなたの準備が完了するのを待っています" + waitingBoth: "準備してください" + ready: "準備完了" + cancelReady: "準備を再開" + opponentTurn: "相手のターンです" + myTurn: "あなたのターンです" + turnOf: "{name}のターンです" + pastTurnOf: "{name}のターン" + surrender: "投了" + surrendered: "投了により" + drawn: "引き分け" + won: "{name}の勝ち" + black: "黒" + white: "白" + total: "合計" + turnCount: "{count}ターン目" + myGames: "自分の対局" + allGames: "みんなの対局" + ended: "終了" + playing: "対局中" + isLlotheo: "石の少ない方が勝ち(ロセオ)" + loopedMap: "ループマップ" + canPutEverywhere: "どこでも置けるモード" diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index b9b44b45b1..9eb5301d93 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -162,7 +162,7 @@ export interface AdminEventTypes { export interface ReversiEventTypes { matched: { - game: Packed<'ReversiGame'>; + game: Packed<'ReversiGameDetailed'>; }; invited: { game: Packed<'ReversiMatching'>; @@ -180,18 +180,21 @@ export interface ReversiGameEventTypes { key: string; value: any; }; - putStone: { - pos: number; - }; +putStone: { +at: Date; +color: boolean; +pos: number; +next: boolean; +}; syncState: { crc32: string; }; started: { - game: Packed<'ReversiGame'>; + game: Packed<'ReversiGameDetailed'>; }; ended: { winnerId: MiUser['id'] | null; - game: Packed<'ReversiGame'>; + game: Packed<'ReversiGameDetailed'>; }; } //#endregion diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 0f264a2ed3..6e88270973 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -86,7 +86,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { isLlotheo: false, }).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0])); - const packed = await this.reversiGameEntityService.pack(game, { id: exist.parentId }); + const packed = await this.reversiGameEntityService.packDetail(game, { id: exist.parentId }); this.globalEventService.publishReversiStream(exist.parentId, 'matched', { game: packed }); return game; @@ -202,13 +202,13 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { this.globalEventService.publishReversiGameStream(game.id, 'ended', { winnerId: winner, - game: await this.reversiGameEntityService.pack(game.id, user), + game: await this.reversiGameEntityService.packDetail(game.id, user), }); } //#endregion this.globalEventService.publishReversiGameStream(game.id, 'started', { - game: await this.reversiGameEntityService.pack(game.id, user), + game: await this.reversiGameEntityService.packDetail(game.id, user), }); }, 3000); } @@ -288,14 +288,14 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { logs: game.logs, }); - this.globalEventService.publishReversiGameStream(game.id, 'set', Object.assign(log, { + this.globalEventService.publishReversiGameStream(game.id, 'putStone', Object.assign(log, { next: o.turn, })); if (o.isEnded) { this.globalEventService.publishReversiGameStream(game.id, 'ended', { winnerId: winner, - game: await this.reversiGameEntityService.pack(game.id, user), + game: await this.reversiGameEntityService.packDetail(game.id, user), }); } } @@ -315,7 +315,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { this.globalEventService.publishReversiGameStream(game.id, 'ended', { winnerId: winnerId, - game: await this.reversiGameEntityService.pack(game.id, user), + game: await this.reversiGameEntityService.packDetail(game.id, user), }); } diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts index 0bf02990d7..cdbf0ffdc3 100644 --- a/packages/backend/src/core/entities/ReversiGameEntityService.ts +++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts @@ -27,14 +27,10 @@ export class ReversiGameEntityService { } @bindThis - public async pack( + public async packDetail( src: MiReversiGame['id'] | MiReversiGame, me?: { id: MiUser['id'] } | null | undefined, - options?: { - detail?: boolean; - skipHide?: boolean; - }, - ): Promise> { + ): Promise> { const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); return await awaitAll({ @@ -59,23 +55,61 @@ export class ReversiGameEntityService { isLlotheo: game.isLlotheo, canPutEverywhere: game.canPutEverywhere, loopedBoard: game.loopedBoard, - ...(options?.detail ? { - logs: game.logs.map(log => ({ - at: log.at.toISOString(), - color: log.color, - pos: log.pos, - })), - map: game.map, - } : {}), + logs: game.logs.map(log => ({ + at: log.at.toISOString(), + color: log.color, + pos: log.pos, + })), + map: game.map, }); } @bindThis - public packMany( + public packDetailMany( xs: MiReversiGame[], me?: { id: MiUser['id'] } | null | undefined, ) { - return Promise.all(xs.map(x => this.pack(x, me))); + return Promise.all(xs.map(x => this.packDetail(x, me))); + } + + @bindThis + public async packLite( + src: MiReversiGame['id'] | MiReversiGame, + me?: { id: MiUser['id'] } | null | undefined, + ): Promise> { + const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: game.id, + createdAt: this.idService.parse(game.id).date.toISOString(), + startedAt: game.startedAt && game.startedAt.toISOString(), + isStarted: game.isStarted, + isEnded: game.isEnded, + form1: game.form1, + form2: game.form2, + user1Accepted: game.user1Accepted, + user2Accepted: game.user2Accepted, + user1Id: game.user1Id, + user2Id: game.user2Id, + user1: this.userEntityService.pack(game.user1Id, me), + user2: this.userEntityService.pack(game.user2Id, me), + winnerId: game.winnerId, + winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null, + surrendered: game.surrendered, + black: game.black, + bw: game.bw, + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + }); + } + + @bindThis + public packLiteMany( + xs: MiReversiGame[], + me?: { id: MiUser['id'] } | null | undefined, + ) { + return Promise.all(xs.map(x => this.packLite(x, me))); } } diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index cabee95dad..3ee700fef7 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -39,7 +39,7 @@ import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js'; import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js'; import { packedAdSchema } from '@/models/json-schema/ad.js'; -import { packedReversiGameSchema } from '@/models/json-schema/reversi-game.js'; +import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js'; import { packedReversiMatchingSchema } from '@/models/json-schema/reversi-matching.js'; export const refs = { @@ -80,7 +80,8 @@ export const refs = { Signin: packedSigninSchema, RoleLite: packedRoleLiteSchema, Role: packedRoleSchema, - ReversiGame: packedReversiGameSchema, + ReversiGameLite: packedReversiGameLiteSchema, + ReversiGameDetailed: packedReversiGameDetailedSchema, ReversiMatching: packedReversiMatchingSchema, }; diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts index fc7cdb063d..7ffb447db5 100644 --- a/packages/backend/src/models/json-schema/reversi-game.ts +++ b/packages/backend/src/models/json-schema/reversi-game.ts @@ -3,7 +3,107 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export const packedReversiGameSchema = { +export const packedReversiGameLiteSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + startedAt: { + type: 'string', + optional: false, nullable: true, + format: 'date-time', + }, + isStarted: { + type: 'boolean', + optional: false, nullable: false, + }, + isEnded: { + type: 'boolean', + optional: false, nullable: false, + }, + form1: { + type: 'any', + optional: false, nullable: true, + }, + form2: { + type: 'any', + optional: false, nullable: true, + }, + user1Accepted: { + type: 'boolean', + optional: false, nullable: false, + }, + user2Accepted: { + type: 'boolean', + optional: false, nullable: false, + }, + user1Id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user2Id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + user1: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, + user2: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, + winnerId: { + type: 'string', + optional: false, nullable: true, + format: 'id', + }, + winner: { + type: 'object', + optional: false, nullable: true, + ref: 'User', + }, + surrendered: { + type: 'string', + optional: false, nullable: true, + format: 'id', + }, + black: { + type: 'number', + optional: false, nullable: true, + }, + bw: { + type: 'string', + optional: false, nullable: false, + }, + isLlotheo: { + type: 'boolean', + optional: false, nullable: false, + }, + canPutEverywhere: { + type: 'boolean', + optional: false, nullable: false, + }, + loopedBoard: { + type: 'boolean', + optional: false, nullable: false, + }, + }, +} as const; + +export const packedReversiGameDetailedSchema = { type: 'object', properties: { id: { @@ -102,10 +202,10 @@ export const packedReversiGameSchema = { }, logs: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'object', - optional: true, nullable: false, + optional: false, nullable: false, properties: { at: { type: 'string', @@ -125,7 +225,7 @@ export const packedReversiGameSchema = { }, map: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/reversi/games.ts b/packages/backend/src/server/api/endpoints/reversi/games.ts index 420e80ccbe..5322cd0987 100644 --- a/packages/backend/src/server/api/endpoints/reversi/games.ts +++ b/packages/backend/src/server/api/endpoints/reversi/games.ts @@ -17,7 +17,7 @@ export const meta = { res: { type: 'array', optional: false, nullable: false, - items: { ref: 'ReversiGame' }, + items: { ref: 'ReversiGameLite' }, }, } as const; @@ -55,7 +55,7 @@ export default class extends Endpoint { // eslint- const games = await query.take(ps.limit).getMany(); - return await this.reversiGameEntityService.packMany(games, me); + return await this.reversiGameEntityService.packLiteMany(games, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts index 850c65d5ff..952d58ff6f 100644 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -60,7 +60,7 @@ export default class extends Endpoint { // eslint- if (game == null) return; - return await this.reversiGameEntityService.pack(game, me); + return await this.reversiGameEntityService.packDetail(game, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/reversi/show-game.ts b/packages/backend/src/server/api/endpoints/reversi/show-game.ts index e036dd6c76..de571053e1 100644 --- a/packages/backend/src/server/api/endpoints/reversi/show-game.ts +++ b/packages/backend/src/server/api/endpoints/reversi/show-game.ts @@ -23,7 +23,7 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - ref: 'ReversiGame', + ref: 'ReversiGameDetailed', }, } as const; @@ -48,7 +48,7 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchGame); } - return await this.reversiGameEntityService.pack(game, me); + return await this.reversiGameEntityService.packDetail(game, me); }); } } 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 4234da2624..1e5b43783e 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -92,7 +92,7 @@ class ReversiGameChannel extends Channel { if (!game.isStarted) return; if (crc32.toString() !== game.crc32) { - this.send('rescue', await this.reversiGameEntityService.pack(game, this.user)); + this.send('rescue', await this.reversiGameEntityService.packDetail(game, this.user)); } } diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue new file mode 100644 index 0000000000..b69d94e9c2 --- /dev/null +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -0,0 +1,534 @@ + + + + + + + diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue new file mode 100644 index 0000000000..57cb8d907e --- /dev/null +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue index c0f4668b7b..37d22ae357 100644 --- a/packages/frontend/src/pages/reversi/game.vue +++ b/packages/frontend/src/pages/reversi/game.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> @@ -22,7 +22,7 @@ const props = defineProps<{ gameId: string; }>(); -const game = shallowRef(null); +const game = shallowRef(null); const connection = shallowRef(null); watch(() => props.gameId, () => { diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue new file mode 100644 index 0000000000..8474b782ab --- /dev/null +++ b/packages/frontend/src/pages/reversi/index.vue @@ -0,0 +1,284 @@ + + + + + + + diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index a20d77b8c2..9203474e7c 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1670,7 +1670,8 @@ declare namespace entities { Signin, RoleLite, Role, - ReversiGame, + ReversiGameLite, + ReversiGameDetailed, ReversiMatching } } @@ -2612,7 +2613,10 @@ type RetentionResponse = operations['retention']['responses']['200']['content'][ type ReversiCancelMatchResponse = operations['reversi/cancel-match']['responses']['200']['content']['application/json']; // @public (undocumented) -type ReversiGame = components['schemas']['ReversiGame']; +type ReversiGameDetailed = components['schemas']['ReversiGameDetailed']; + +// @public (undocumented) +type ReversiGameLite = components['schemas']['ReversiGameLite']; // @public (undocumented) type ReversiGamesRequest = operations['reversi/games']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 5f61a719bc..cba6bdcfeb 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-18T11:02:35.535Z + * generatedAt: 2024-01-18T11:53:06.433Z */ import type { SwitchCaseResponseType } from '../api.js'; diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 53bfc84e22..354252a801 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-18T11:02:35.533Z + * generatedAt: 2024-01-18T11:53:06.431Z */ import type { diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 0ac45663ab..ed85c388c4 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-18T11:02:35.532Z + * generatedAt: 2024-01-18T11:53:06.430Z */ import { operations } from './types.js'; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 217733874c..40ecb6df21 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-18T11:02:35.530Z + * generatedAt: 2024-01-18T11:53:06.429Z */ import { components } from './types.js'; @@ -41,5 +41,6 @@ export type Flash = components['schemas']['Flash']; export type Signin = components['schemas']['Signin']; export type RoleLite = components['schemas']['RoleLite']; export type Role = components['schemas']['Role']; -export type ReversiGame = components['schemas']['ReversiGame']; +export type ReversiGameLite = components['schemas']['ReversiGameLite']; +export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed']; export type ReversiMatching = components['schemas']['ReversiMatching']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 87e10a58e6..db0d9885b2 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3,7 +3,7 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-18T11:02:35.451Z + * generatedAt: 2024-01-18T11:53:06.350Z */ /** @@ -4458,7 +4458,7 @@ export type components = { }; usersCount: number; }); - ReversiGame: { + ReversiGameLite: { /** Format: id */ id: string; /** Format: date-time */ @@ -4487,13 +4487,43 @@ export type components = { isLlotheo: boolean; canPutEverywhere: boolean; loopedBoard: boolean; - logs?: { + }; + ReversiGameDetailed: { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + startedAt: string | null; + isStarted: boolean; + isEnded: boolean; + form1: Record | null; + form2: Record | null; + user1Accepted: boolean; + user2Accepted: boolean; + /** Format: id */ + user1Id: string; + /** Format: id */ + user2Id: string; + user1: components['schemas']['User']; + user2: components['schemas']['User']; + /** Format: id */ + winnerId: string | null; + winner: components['schemas']['User'] | null; + /** Format: id */ + surrendered: string | null; + black: number | null; + bw: string; + isLlotheo: boolean; + canPutEverywhere: boolean; + loopedBoard: boolean; + logs: { /** Format: date-time */ at: string; color: boolean; pos: number; }[]; - map?: string[]; + map: string[]; }; ReversiMatching: { /** Format: id */ @@ -25716,7 +25746,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['ReversiGame'][]; + 'application/json': components['schemas']['ReversiGameLite'][]; }; }; /** @description Client error */ @@ -25878,7 +25908,7 @@ export type operations = { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['ReversiGame']; + 'application/json': components['schemas']['ReversiGameDetailed']; }; }; /** @description Client error */ From 037a1daa7995f5e7f291b83e1a50ea00ce0ccb97 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 19 Jan 2024 10:59:55 +0900 Subject: [PATCH 0016/1120] wip --- packages/backend/src/core/CoreModule.ts | 6 -- .../backend/src/core/GlobalEventService.ts | 14 +-- packages/backend/src/core/ReversiService.ts | 97 ++++++++++++------- .../entities/ReversiMatchingEntityService.ts | 57 ----------- packages/backend/src/di-symbols.ts | 1 - packages/backend/src/misc/json-schema.ts | 2 - .../backend/src/models/RepositoryModule.ts | 10 +- .../backend/src/models/ReversiMatching.ts | 29 ------ packages/backend/src/models/_.ts | 3 - .../models/json-schema/reversi-matching.ts | 40 -------- packages/backend/src/postgres.ts | 2 - .../api/endpoints/reversi/cancel-match.ts | 8 +- .../api/endpoints/reversi/invitations.ts | 23 ++--- .../src/server/api/endpoints/reversi/match.ts | 10 +- packages/misskey-js/etc/misskey-js.api.md | 14 +-- .../misskey-js/src/autogen/apiClientJSDoc.ts | 2 +- packages/misskey-js/src/autogen/endpoint.ts | 8 +- packages/misskey-js/src/autogen/entities.ts | 4 +- packages/misskey-js/src/autogen/models.ts | 3 +- packages/misskey-js/src/autogen/types.ts | 34 +++---- 20 files changed, 114 insertions(+), 253 deletions(-) delete mode 100644 packages/backend/src/core/entities/ReversiMatchingEntityService.ts delete mode 100644 packages/backend/src/models/ReversiMatching.ts delete mode 100644 packages/backend/src/models/json-schema/reversi-matching.ts diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 0ad91ff308..c9e285346e 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -116,7 +116,6 @@ import { FlashEntityService } from './entities/FlashEntityService.js'; import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js'; import { RoleEntityService } from './entities/RoleEntityService.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; -import { ReversiMatchingEntityService } from './entities/ReversiMatchingEntityService.js'; import { ApAudienceService } from './activitypub/ApAudienceService.js'; import { ApDbResolverService } from './activitypub/ApDbResolverService.js'; @@ -255,7 +254,6 @@ const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisti const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService }; -const $ReversiMatchingEntityService: Provider = { provide: 'ReversiMatchingEntityService', useExisting: ReversiMatchingEntityService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; @@ -395,7 +393,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FlashLikeEntityService, RoleEntityService, ReversiGameEntityService, - ReversiMatchingEntityService, ApAudienceService, ApDbResolverService, @@ -531,7 +528,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FlashLikeEntityService, $RoleEntityService, $ReversiGameEntityService, - $ReversiMatchingEntityService, $ApAudienceService, $ApDbResolverService, @@ -667,7 +663,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FlashLikeEntityService, RoleEntityService, ReversiGameEntityService, - ReversiMatchingEntityService, ApAudienceService, ApDbResolverService, @@ -802,7 +797,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FlashLikeEntityService, $RoleEntityService, $ReversiGameEntityService, - $ReversiMatchingEntityService, $ApAudienceService, $ApDbResolverService, diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 9eb5301d93..aa812c7a99 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -165,7 +165,7 @@ export interface ReversiEventTypes { game: Packed<'ReversiGameDetailed'>; }; invited: { - game: Packed<'ReversiMatching'>; + user: Packed<'User'>; }; } @@ -180,12 +180,12 @@ export interface ReversiGameEventTypes { key: string; value: any; }; -putStone: { -at: Date; -color: boolean; -pos: number; -next: boolean; -}; + putStone: { + at: Date; + color: boolean; + pos: number; + next: boolean; + }; syncState: { crc32: string; }; diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts index 6e88270973..f5383af058 100644 --- a/packages/backend/src/core/ReversiService.ts +++ b/packages/backend/src/core/ReversiService.ts @@ -8,10 +8,10 @@ import * as Redis from 'ioredis'; import * as CRC32 from 'crc-32'; import { ModuleRef } from '@nestjs/core'; import * as Reversi from 'misskey-reversi'; +import { IsNull } from 'typeorm'; import type { MiReversiGame, ReversiGamesRepository, - ReversiMatchingsRepository, UsersRepository, } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; @@ -25,10 +25,11 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { IdService } from '@/core/IdService.js'; import type { Packed } from '@/misc/json-schema.js'; import { NotificationService } from '@/core/NotificationService.js'; -import { ReversiMatchingEntityService } from '@/core/entities/ReversiMatchingEntityService.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; +const MATCHING_TIMEOUT_MS = 15 * 1000; // 15sec + @Injectable() export class ReversiService implements OnApplicationShutdown, OnModuleInit { private notificationService: NotificationService; @@ -36,20 +37,16 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { constructor( private moduleRef: ModuleRef, - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, + @Inject(DI.redis) + private redisClient: Redis.Redis, @Inject(DI.reversiGamesRepository) private reversiGamesRepository: ReversiGamesRepository, - @Inject(DI.reversiMatchingsRepository) - private reversiMatchingsRepository: ReversiMatchingsRepository, - private cacheService: CacheService, private userEntityService: UserEntityService, private globalEventService: GlobalEventService, private reversiGameEntityService: ReversiGameEntityService, - private reversiMatchingsEntityService: ReversiMatchingEntityService, private idService: IdService, ) { } @@ -59,22 +56,19 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } @bindThis - public async match(me: MiUser, targetUser: MiUser): Promise { + public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise { if (targetUser.id === me.id) { throw new Error('You cannot match yourself.'); } - const exist = await this.reversiMatchingsRepository.findOneBy({ - parentId: targetUser.id, - childId: me.id, - }); + const invitations = await this.redisClient.zrange(`reversi:matchSpecific:${me.id}`, Date.now() - MATCHING_TIMEOUT_MS, '+inf'); - if (exist) { - this.reversiMatchingsRepository.delete(exist.id); + if (invitations.includes(targetUser.id)) { + await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id); const game = await this.reversiGamesRepository.insert({ id: this.idService.gen(), - user1Id: exist.parentId, + user1Id: targetUser.id, user2Id: me.id, user1Accepted: false, user2Accepted: false, @@ -86,35 +80,64 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { isLlotheo: false, }).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0])); - const packed = await this.reversiGameEntityService.packDetail(game, { id: exist.parentId }); - this.globalEventService.publishReversiStream(exist.parentId, 'matched', { game: packed }); + const packed = await this.reversiGameEntityService.packDetail(game, { id: targetUser.id }); + this.globalEventService.publishReversiStream(targetUser.id, 'matched', { game: packed }); return game; } else { - const child = targetUser; + this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id); - await this.reversiMatchingsRepository.delete({ - parentId: me.id, + this.globalEventService.publishReversiStream(targetUser.id, 'invited', { + user: await this.userEntityService.pack(me, targetUser), }); - const matching = await this.reversiMatchingsRepository.insert({ - id: this.idService.gen(), - parentId: me.id, - childId: child.id, - }).then(x => this.reversiMatchingsRepository.findOneByOrFail(x.identifiers[0])); - - const packed = await this.reversiMatchingsEntityService.pack(matching, child); - this.globalEventService.publishReversiStream(child.id, 'invited', { game: packed }); - return null; } } @bindThis - public async matchCancel(user: MiUser) { - await this.reversiMatchingsRepository.delete({ - parentId: user.id, - }); + public async matchAnyUser(me: MiUser): Promise { + const scanRes = await this.redisClient.scan(0, 'MATCH', 'reversi:matchAny:*', 'COUNT', 10); + const userIds = scanRes[1].map(key => key.split(':')[2]).filter(id => id !== me.id); + + if (userIds.length > 0) { + // pick random + const matchedUserId = userIds[Math.floor(Math.random() * userIds.length)]; + + await this.redisClient.del(`reversi:matchAny:${matchedUserId}`); + + const game = await this.reversiGamesRepository.insert({ + id: this.idService.gen(), + user1Id: matchedUserId, + user2Id: me.id, + user1Accepted: false, + user2Accepted: false, + isStarted: false, + isEnded: false, + logs: [], + map: Reversi.maps.eighteight.data, + bw: 'random', + isLlotheo: false, + }).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0])); + + const packed = await this.reversiGameEntityService.packDetail(game, { id: matchedUserId }); + this.globalEventService.publishReversiStream(matchedUserId, 'matched', { game: packed }); + + return game; + } else { + await this.redisClient.setex(`reversi:matchAny:${me.id}`, MATCHING_TIMEOUT_MS / 1000, ''); + return null; + } + } + + @bindThis + public async matchSpecificUserCancel(user: MiUser, targetUserId: MiUser['id']) { + await this.redisClient.zrem(`reversi:matchSpecific:${targetUserId}`, user.id); + } + + @bindThis + public async matchAnyUserCancel(user: MiUser) { + await this.redisClient.del(`reversi:matchAny:${user.id}`); } @bindThis @@ -214,6 +237,12 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit { } } + @bindThis + public async getInvitations(user: MiUser): Promise { + const invitations = await this.redisClient.zrange(`reversi:matchSpecific:${user.id}`, Date.now() - MATCHING_TIMEOUT_MS, '+inf'); + return invitations; + } + @bindThis public async updateSettings(game: MiReversiGame, user: MiUser, key: string, value: any) { if (game.isStarted) return; diff --git a/packages/backend/src/core/entities/ReversiMatchingEntityService.ts b/packages/backend/src/core/entities/ReversiMatchingEntityService.ts deleted file mode 100644 index 1b39679ec1..0000000000 --- a/packages/backend/src/core/entities/ReversiMatchingEntityService.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { DI } from '@/di-symbols.js'; -import type { MiReversiMatching, ReversiMatchingsRepository } from '@/models/_.js'; -import { awaitAll } from '@/misc/prelude/await-all.js'; -import type { Packed } from '@/misc/json-schema.js'; -import type { } from '@/models/Blocking.js'; -import type { MiUser } from '@/models/User.js'; -import { bindThis } from '@/decorators.js'; -import { IdService } from '@/core/IdService.js'; -import { UserEntityService } from './UserEntityService.js'; - -@Injectable() -export class ReversiMatchingEntityService { - constructor( - @Inject(DI.reversiMatchingsRepository) - private reversiMatchingsRepository: ReversiMatchingsRepository, - - private userEntityService: UserEntityService, - private idService: IdService, - ) { - } - - @bindThis - public async pack( - src: MiReversiMatching['id'] | MiReversiMatching, - me?: { id: MiUser['id'] } | null | undefined, - ): Promise> { - const matching = typeof src === 'object' ? src : await this.reversiMatchingsRepository.findOneByOrFail({ id: src }); - - return await awaitAll({ - id: matching.id, - createdAt: this.idService.parse(matching.id).date.toISOString(), - parentId: matching.parentId, - parent: this.userEntityService.pack(matching.parentId, me, { - detail: true, - }), - childId: matching.childId, - child: this.userEntityService.pack(matching.childId, me, { - detail: true, - }), - }); - } - - @bindThis - public packMany( - xs: MiReversiMatching[], - me?: { id: MiUser['id'] } | null | undefined, - ) { - return Promise.all(xs.map(x => this.pack(x, me))); - } -} - diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 841a9e9bb6..73de01f33a 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -80,6 +80,5 @@ export const DI = { userMemosRepository: Symbol('userMemosRepository'), bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), reversiGamesRepository: Symbol('reversiGamesRepository'), - reversiMatchingsRepository: Symbol('reversiMatchingsRepository'), //#endregion }; diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 3ee700fef7..b4f0541712 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -40,7 +40,6 @@ import { packedSigninSchema } from '@/models/json-schema/signin.js'; import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js'; import { packedAdSchema } from '@/models/json-schema/ad.js'; import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js'; -import { packedReversiMatchingSchema } from '@/models/json-schema/reversi-matching.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -82,7 +81,6 @@ export const refs = { Role: packedRoleSchema, ReversiGameLite: packedReversiGameLiteSchema, ReversiGameDetailed: packedReversiGameDetailedSchema, - ReversiMatching: packedReversiMatchingSchema, }; export type Packed = SchemaType; diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index f2c4e29112..2b2aaeb91c 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -5,7 +5,7 @@ import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame, MiReversiMatching } from './_.js'; +import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame } from './_.js'; import type { DataSource } from 'typeorm'; import type { Provider } from '@nestjs/common'; @@ -411,12 +411,6 @@ const $reversiGamesRepository: Provider = { inject: [DI.db], }; -const $reversiMatchingsRepository: Provider = { - provide: DI.reversiMatchingsRepository, - useFactory: (db: DataSource) => db.getRepository(MiReversiMatching), - inject: [DI.db], -}; - @Module({ imports: [ ], @@ -488,7 +482,6 @@ const $reversiMatchingsRepository: Provider = { $userMemosRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, - $reversiMatchingsRepository, ], exports: [ $usersRepository, @@ -558,7 +551,6 @@ const $reversiMatchingsRepository: Provider = { $userMemosRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, - $reversiMatchingsRepository, ], }) export class RepositoryModule {} diff --git a/packages/backend/src/models/ReversiMatching.ts b/packages/backend/src/models/ReversiMatching.ts deleted file mode 100644 index 1e31fca901..0000000000 --- a/packages/backend/src/models/ReversiMatching.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('reversi_matching') -export class MiReversiMatching { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public parentId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public parent: MiUser | null; - - @Index() - @Column(id()) - public childId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public child: MiUser | null; -} diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 0b097caf95..a1a0d8823d 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -70,7 +70,6 @@ import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserListFavorite } from '@/models/UserListFavorite.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; -import { MiReversiMatching } from '@/models/ReversiMatching.js'; import type { Repository } from 'typeorm'; @@ -142,7 +141,6 @@ export { MiUserMemo, MiBubbleGameRecord, MiReversiGame, - MiReversiMatching, }; export type AbuseUserReportsRepository = Repository; @@ -212,4 +210,3 @@ export type FlashLikesRepository = Repository; export type UserMemoRepository = Repository; export type BubbleGameRecordsRepository = Repository; export type ReversiGamesRepository = Repository; -export type ReversiMatchingsRepository = Repository; diff --git a/packages/backend/src/models/json-schema/reversi-matching.ts b/packages/backend/src/models/json-schema/reversi-matching.ts deleted file mode 100644 index a98b0a1644..0000000000 --- a/packages/backend/src/models/json-schema/reversi-matching.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const packedReversiMatchingSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - parentId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - parent: { - type: 'object', - optional: false, nullable: true, - ref: 'User', - }, - childId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - child: { - type: 'object', - optional: false, nullable: false, - ref: 'User', - }, - }, -} as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 5435696b14..1e063c8673 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -78,7 +78,6 @@ import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserMemo } from '@/models/UserMemo.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; -import { MiReversiMatching } from '@/models/ReversiMatching.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; @@ -195,7 +194,6 @@ export const entities = [ MiUserMemo, MiBubbleGameRecord, MiReversiGame, - MiReversiMatching, ...charts, ]; diff --git a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts index cacbc6d85e..8edc049500 100644 --- a/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/cancel-match.ts @@ -22,6 +22,7 @@ export const meta = { export const paramDef = { type: 'object', properties: { + userId: { type: 'string', format: 'misskey:id', nullable: true }, }, required: [], } as const; @@ -32,7 +33,12 @@ export default class extends Endpoint { // eslint- private reversiService: ReversiService, ) { super(meta, paramDef, async (ps, me) => { - await this.reversiService.matchCancel(me); + if (ps.userId) { + await this.reversiService.matchSpecificUserCancel(me, ps.userId); + return; + } else { + await this.reversiService.matchAnyUserCancel(me); + } }); } } diff --git a/packages/backend/src/server/api/endpoints/reversi/invitations.ts b/packages/backend/src/server/api/endpoints/reversi/invitations.ts index da6cf66991..0b7107bb0d 100644 --- a/packages/backend/src/server/api/endpoints/reversi/invitations.ts +++ b/packages/backend/src/server/api/endpoints/reversi/invitations.ts @@ -6,8 +6,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; -import { ReversiMatchingEntityService } from '@/core/entities/ReversiMatchingEntityService.js'; -import type { ReversiMatchingsRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ReversiService } from '@/core/ReversiService.js'; export const meta = { requireCredential: true, @@ -17,32 +17,23 @@ export const meta = { res: { type: 'array', optional: false, nullable: false, - items: { ref: 'ReversiMatching' }, + items: { ref: 'UserLite' }, }, } as const; export const paramDef = { - type: 'object', - properties: { - userId: { type: 'string', format: 'misskey:id' }, - }, - required: ['userId'], } as const; @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.reversiMatchingsRepository) - private reversiMatchingsRepository: ReversiMatchingsRepository, - - private reversiMatchingEntityService: ReversiMatchingEntityService, + private userEntityService: UserEntityService, + private reversiService: ReversiService, ) { super(meta, paramDef, async (ps, me) => { - const invitations = await this.reversiMatchingsRepository.findBy({ - childId: me.id, - }); + const invitations = await this.reversiService.getInvitations(me); - return await this.reversiMatchingEntityService.packMany(invitations, me); + return await this.userEntityService.packMany(invitations, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/reversi/match.ts b/packages/backend/src/server/api/endpoints/reversi/match.ts index 952d58ff6f..da5a3409ef 100644 --- a/packages/backend/src/server/api/endpoints/reversi/match.ts +++ b/packages/backend/src/server/api/endpoints/reversi/match.ts @@ -36,9 +36,9 @@ export const meta = { export const paramDef = { type: 'object', properties: { - userId: { type: 'string', format: 'misskey:id' }, + userId: { type: 'string', format: 'misskey:id', nullable: true }, }, - required: ['userId'], + required: [], } as const; @Injectable() @@ -51,12 +51,12 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { if (ps.userId === me.id) throw new ApiError(meta.errors.isYourself); - const child = await this.getterService.getUser(ps.userId).catch(err => { + const target = ps.userId ? await this.getterService.getUser(ps.userId).catch(err => { if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); throw err; - }); + }) : null; - const game = await this.reversiService.match(me, child); + const game = target ? await this.reversiService.matchSpecificUser(me, target) : await this.reversiService.matchAnyUser(me); if (game == null) return; diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 9203474e7c..2b95e01533 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1623,12 +1623,12 @@ declare namespace entities { BubbleGameRegisterResponse, BubbleGameRankingRequest, BubbleGameRankingResponse, + ReversiCancelMatchRequest, ReversiCancelMatchResponse, ReversiGamesRequest, ReversiGamesResponse, ReversiMatchRequest, ReversiMatchResponse, - ReversiInvitationsRequest, ReversiInvitationsResponse, ReversiShowGameRequest, ReversiShowGameResponse, @@ -1671,8 +1671,7 @@ declare namespace entities { RoleLite, Role, ReversiGameLite, - ReversiGameDetailed, - ReversiMatching + ReversiGameDetailed } } export { entities } @@ -2609,6 +2608,9 @@ type ResetPasswordRequest = operations['reset-password']['requestBody']['content // @public (undocumented) type RetentionResponse = operations['retention']['responses']['200']['content']['application/json']; +// @public (undocumented) +type ReversiCancelMatchRequest = operations['reversi/cancel-match']['requestBody']['content']['application/json']; + // @public (undocumented) type ReversiCancelMatchResponse = operations['reversi/cancel-match']['responses']['200']['content']['application/json']; @@ -2624,15 +2626,9 @@ type ReversiGamesRequest = operations['reversi/games']['requestBody']['content'] // @public (undocumented) type ReversiGamesResponse = operations['reversi/games']['responses']['200']['content']['application/json']; -// @public (undocumented) -type ReversiInvitationsRequest = operations['reversi/invitations']['requestBody']['content']['application/json']; - // @public (undocumented) type ReversiInvitationsResponse = operations['reversi/invitations']['responses']['200']['content']['application/json']; -// @public (undocumented) -type ReversiMatching = components['schemas']['ReversiMatching']; - // @public (undocumented) type ReversiMatchRequest = operations['reversi/match']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index cba6bdcfeb..7b71452292 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-18T11:53:06.433Z + * generatedAt: 2024-01-19T01:59:26.059Z */ import type { SwitchCaseResponseType } from '../api.js'; diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 354252a801..aa2acad2f2 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-18T11:53:06.431Z + * generatedAt: 2024-01-19T01:59:26.057Z */ import type { @@ -544,12 +544,12 @@ import type { BubbleGameRegisterResponse, BubbleGameRankingRequest, BubbleGameRankingResponse, + ReversiCancelMatchRequest, ReversiCancelMatchResponse, ReversiGamesRequest, ReversiGamesResponse, ReversiMatchRequest, ReversiMatchResponse, - ReversiInvitationsRequest, ReversiInvitationsResponse, ReversiShowGameRequest, ReversiShowGameResponse, @@ -917,10 +917,10 @@ export type Endpoints = { 'retention': { req: EmptyRequest; res: RetentionResponse }; 'bubble-game/register': { req: BubbleGameRegisterRequest; res: BubbleGameRegisterResponse }; 'bubble-game/ranking': { req: BubbleGameRankingRequest; res: BubbleGameRankingResponse }; - 'reversi/cancel-match': { req: EmptyRequest; res: ReversiCancelMatchResponse }; + 'reversi/cancel-match': { req: ReversiCancelMatchRequest; res: ReversiCancelMatchResponse }; 'reversi/games': { req: ReversiGamesRequest; res: ReversiGamesResponse }; 'reversi/match': { req: ReversiMatchRequest; res: ReversiMatchResponse }; - 'reversi/invitations': { req: ReversiInvitationsRequest; res: ReversiInvitationsResponse }; + 'reversi/invitations': { req: EmptyRequest; res: ReversiInvitationsResponse }; 'reversi/show-game': { req: ReversiShowGameRequest; res: ReversiShowGameResponse }; 'reversi/surrender': { req: ReversiSurrenderRequest; res: EmptyResponse }; } diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index ed85c388c4..bae3fc9d04 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-18T11:53:06.430Z + * generatedAt: 2024-01-19T01:59:26.055Z */ import { operations } from './types.js'; @@ -546,12 +546,12 @@ export type BubbleGameRegisterRequest = operations['bubble-game/register']['requ export type BubbleGameRegisterResponse = operations['bubble-game/register']['responses']['200']['content']['application/json']; export type BubbleGameRankingRequest = operations['bubble-game/ranking']['requestBody']['content']['application/json']; export type BubbleGameRankingResponse = operations['bubble-game/ranking']['responses']['200']['content']['application/json']; +export type ReversiCancelMatchRequest = operations['reversi/cancel-match']['requestBody']['content']['application/json']; export type ReversiCancelMatchResponse = operations['reversi/cancel-match']['responses']['200']['content']['application/json']; export type ReversiGamesRequest = operations['reversi/games']['requestBody']['content']['application/json']; export type ReversiGamesResponse = operations['reversi/games']['responses']['200']['content']['application/json']; export type ReversiMatchRequest = operations['reversi/match']['requestBody']['content']['application/json']; export type ReversiMatchResponse = operations['reversi/match']['responses']['200']['content']['application/json']; -export type ReversiInvitationsRequest = operations['reversi/invitations']['requestBody']['content']['application/json']; export type ReversiInvitationsResponse = operations['reversi/invitations']['responses']['200']['content']['application/json']; export type ReversiShowGameRequest = operations['reversi/show-game']['requestBody']['content']['application/json']; export type ReversiShowGameResponse = operations['reversi/show-game']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 40ecb6df21..81bfab2ece 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -1,6 +1,6 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-18T11:53:06.429Z + * generatedAt: 2024-01-19T01:59:26.054Z */ import { components } from './types.js'; @@ -43,4 +43,3 @@ export type RoleLite = components['schemas']['RoleLite']; export type Role = components['schemas']['Role']; export type ReversiGameLite = components['schemas']['ReversiGameLite']; export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed']; -export type ReversiMatching = components['schemas']['ReversiMatching']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index db0d9885b2..fe583dc4b9 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3,7 +3,7 @@ /* * version: 2023.12.2 - * generatedAt: 2024-01-18T11:53:06.350Z + * generatedAt: 2024-01-19T01:59:25.971Z */ /** @@ -4525,18 +4525,6 @@ export type components = { }[]; map: string[]; }; - ReversiMatching: { - /** Format: id */ - id: string; - /** Format: date-time */ - createdAt: string; - /** Format: id */ - parentId: string; - parent: components['schemas']['User'] | null; - /** Format: id */ - childId: string; - child: components['schemas']['User']; - }; }; responses: never; parameters: never; @@ -25682,6 +25670,14 @@ export type operations = { * **Credential required**: *Yes* / **Permission**: *write:account* */ 'reversi/cancel-match': { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + userId?: string | null; + }; + }; + }; responses: { /** @description OK (with results) */ 200: { @@ -25792,7 +25788,7 @@ export type operations = { content: { 'application/json': { /** Format: misskey:id */ - userId: string; + userId?: string | null; }; }; }; @@ -25842,19 +25838,11 @@ export type operations = { * **Credential required**: *Yes* / **Permission**: *read:account* */ 'reversi/invitations': { - requestBody: { - content: { - 'application/json': { - /** Format: misskey:id */ - userId: string; - }; - }; - }; responses: { /** @description OK (with results) */ 200: { content: { - 'application/json': components['schemas']['ReversiMatching'][]; + 'application/json': components['schemas']['UserLite'][]; }; }; /** @description Client error */ From d85085d16fb6952e742eeca38b98f9442b7ec984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acid=20Chicken=20=28=E7=A1=AB=E9=85=B8=E9=B6=8F=29?= Date: Fri, 19 Jan 2024 11:54:00 +0900 Subject: [PATCH 0017/1120] refactor: style --- packages/frontend/src/scripts/i18n.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts index 55b5371950..3366f3eac3 100644 --- a/packages/frontend/src/scripts/i18n.ts +++ b/packages/frontend/src/scripts/i18n.ts @@ -48,7 +48,7 @@ export class I18n { } if (typeof value === 'string') { - const parameters = Array.from(value.matchAll(/\{(\w+)\}/g)).map(([, parameter]) => parameter); + const parameters = Array.from(value.matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter); if (parameters.length) { console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`); From 3ad273237557eefd75a715fe3bcb86f3d2703396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:19:06 +0900 Subject: [PATCH 0018/1120] =?UTF-8?q?fix(frontend/HorizontalSwipe):=20?= =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AE=E8=A6=81=E7=B4=A0=E3=81=8C?= =?UTF-8?q?=E3=81=AF=E3=81=BF=E5=87=BA=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#13036)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/components/MkHorizontalSwipe.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue index 2c62aadbf4..a7d0d5a3e4 100644 --- a/packages/frontend/src/components/MkHorizontalSwipe.vue +++ b/packages/frontend/src/components/MkHorizontalSwipe.vue @@ -180,6 +180,7 @@ watch(tabModel, (newTab, oldTab) => { + diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue index 57cb8d907e..d118205b99 100644 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -4,86 +4,82 @@ SPDX-License-Identifier: AGPL-3.0-only --> + + diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue index 37d22ae357..dbbeb20f42 100644 --- a/packages/frontend/src/pages/reversi/game.vue +++ b/packages/frontend/src/pages/reversi/game.vue @@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only - - diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue index d118205b99..301a177de1 100644 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only -
{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}
+
{{ i18n.tsx._initialAccountSetting.haveFun({ name: instance.name ?? host }) }}
{{ i18n.ts.goBack }} {{ i18n.ts.close }} diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue index af094a8e8c..9e3a78fe22 100644 --- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue +++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue @@ -118,7 +118,7 @@ async function done() { async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: title.value }), + text: i18n.tsx.removeAreYouSure({ x: title.value }), }); if (canceled) return; diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue index 37aa677b44..f082833838 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue @@ -68,7 +68,7 @@ function setAvatar(ev) { const { canceled } = await os.confirm({ type: 'question', - text: i18n.t('cropImageAsk'), + text: i18n.ts.cropImageAsk, okText: i18n.ts.cropYes, cancelText: i18n.ts.cropNo, }); diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index 05b55f77a7..4b0c540829 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.pushNotification }}
-
{{ i18n.t('_initialAccountSetting.pushNotificationDescription', { name: instance.name ?? host }) }}
+
{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}
{{ i18n.ts.goBack }} @@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}
-
{{ i18n.t('_initialAccountSetting.youCanContinueTutorial', { name: instance.name ?? host }) }}
+
{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}
{{ i18n.ts._initialAccountSetting.startTutorial }}
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index ee4e29dd8f..b8b253de06 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + {{ i18n.ts.add }} {{ i18n.ts.close }} @@ -109,7 +109,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) { os.contextMenu([{ type: 'label', - text: i18n.t(`_widgets.${widget.name}`), + text: i18n.ts._widgets[widget.name], }, { icon: 'ti ti-settings', text: i18n.ts.settings, diff --git a/packages/frontend/src/components/global/I18n.vue b/packages/frontend/src/components/global/I18n.vue new file mode 100644 index 0000000000..162aa2bcf8 --- /dev/null +++ b/packages/frontend/src/components/global/I18n.vue @@ -0,0 +1,46 @@ + + + diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts index 0eeefa4859..0e7f6a9bdf 100644 --- a/packages/frontend/src/components/global/MkTime.stories.impl.ts +++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts @@ -123,7 +123,7 @@ export const DetailNow = { export const RelativeOneHourAgo = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.t('_ago.hoursAgo', { n: 1 })); + await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.hoursAgo({ n: 1 })); }, args: { ...Empty.args, @@ -162,7 +162,7 @@ export const DetailOneHourAgo = { export const RelativeOneDayAgo = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.t('_ago.daysAgo', { n: 1 })); + await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.daysAgo({ n: 1 })); }, args: { ...Empty.args, @@ -201,7 +201,7 @@ export const DetailOneDayAgo = { export const RelativeOneWeekAgo = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.t('_ago.weeksAgo', { n: 1 })); + await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.weeksAgo({ n: 1 })); }, args: { ...Empty.args, @@ -240,7 +240,7 @@ export const DetailOneWeekAgo = { export const RelativeOneMonthAgo = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.t('_ago.monthsAgo', { n: 1 })); + await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.monthsAgo({ n: 1 })); }, args: { ...Empty.args, @@ -279,7 +279,7 @@ export const DetailOneMonthAgo = { export const RelativeOneYearAgo = { ...Empty, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(i18n.t('_ago.yearsAgo', { n: 1 })); + await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.yearsAgo({ n: 1 })); }, args: { ...Empty.args, diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index e11db9dc31..2b0bf246ad 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -55,21 +55,21 @@ const relative = computed(() => { if (invalid) return i18n.ts._ago.invalid; return ( - ago.value >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago.value / 31536000).toString() }) : - ago.value >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago.value / 2592000).toString() }) : - ago.value >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago.value / 604800).toString() }) : - ago.value >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago.value / 86400).toString() }) : - ago.value >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago.value / 3600).toString() }) : - ago.value >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago.value / 60)).toString() }) : - ago.value >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago.value % 60)).toString() }) : + ago.value >= 31536000 ? i18n.tsx._ago.yearsAgo({ n: Math.round(ago.value / 31536000).toString() }) : + ago.value >= 2592000 ? i18n.tsx._ago.monthsAgo({ n: Math.round(ago.value / 2592000).toString() }) : + ago.value >= 604800 ? i18n.tsx._ago.weeksAgo({ n: Math.round(ago.value / 604800).toString() }) : + ago.value >= 86400 ? i18n.tsx._ago.daysAgo({ n: Math.round(ago.value / 86400).toString() }) : + ago.value >= 3600 ? i18n.tsx._ago.hoursAgo({ n: Math.round(ago.value / 3600).toString() }) : + ago.value >= 60 ? i18n.tsx._ago.minutesAgo({ n: (~~(ago.value / 60)).toString() }) : + ago.value >= 10 ? i18n.tsx._ago.secondsAgo({ n: (~~(ago.value % 60)).toString() }) : ago.value >= -3 ? i18n.ts._ago.justNow : - ago.value < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago.value / 31536000).toString() }) : - ago.value < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago.value / 2592000).toString() }) : - ago.value < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago.value / 604800).toString() }) : - ago.value < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago.value / 86400).toString() }) : - ago.value < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago.value / 3600).toString() }) : - ago.value < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago.value / 60)).toString() }) : - i18n.t('_timeIn.seconds', { n: (~~(-ago.value % 60)).toString() }) + ago.value < -31536000 ? i18n.tsx._timeIn.years({ n: Math.round(-ago.value / 31536000).toString() }) : + ago.value < -2592000 ? i18n.tsx._timeIn.months({ n: Math.round(-ago.value / 2592000).toString() }) : + ago.value < -604800 ? i18n.tsx._timeIn.weeks({ n: Math.round(-ago.value / 604800).toString() }) : + ago.value < -86400 ? i18n.tsx._timeIn.days({ n: Math.round(-ago.value / 86400).toString() }) : + ago.value < -3600 ? i18n.tsx._timeIn.hours({ n: Math.round(-ago.value / 3600).toString() }) : + ago.value < -60 ? i18n.tsx._timeIn.minutes({ n: (~~(-ago.value / 60)).toString() }) : + i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() }) ); }); diff --git a/packages/frontend/src/components/global/i18n.ts b/packages/frontend/src/components/global/i18n.ts deleted file mode 100644 index 2f4d7edabd..0000000000 --- a/packages/frontend/src/components/global/i18n.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { h } from 'vue'; - -export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) { - let str = props.src; - const parsed = [] as (string | { arg: string; })[]; - while (true) { - const nextBracketOpen = str.indexOf('{'); - const nextBracketClose = str.indexOf('}'); - - if (nextBracketOpen === -1) { - parsed.push(str); - break; - } else { - if (nextBracketOpen > 0) parsed.push(str.substring(0, nextBracketOpen)); - parsed.push({ - arg: str.substring(nextBracketOpen + 1, nextBracketClose), - }); - } - - str = str.substring(nextBracketClose + 1); - } - - return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]())); -} diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index a3e13c3a50..f3b476b15c 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -16,7 +16,7 @@ import MkUserName from './global/MkUserName.vue'; import MkEllipsis from './global/MkEllipsis.vue'; import MkTime from './global/MkTime.vue'; import MkUrl from './global/MkUrl.vue'; -import I18n from './global/i18n.js'; +import I18n from './global/I18n.vue'; import RouterView from './global/RouterView.vue'; import MkLoading from './global/MkLoading.vue'; import MkError from './global/MkError.vue'; diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index 4ba1b6da76..69cb6ef647 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
{{ i18n.ts.aboutMisskey }}
diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index 4a9c659a97..84f3d1f3f1 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -104,7 +104,7 @@ fetch(); async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: file.value.name }), + text: i18n.tsx.removeAreYouSure({ x: file.value.name }), }); if (canceled) return; diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index 85417f0ecb..530bcca04a 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -182,9 +182,9 @@ SPDX-License-Identifier: AGPL-3.0-only
-
{{ i18n.t('recentNHours', { n: 90 }) }}
+
{{ i18n.tsx.recentNHours({ n: 90 }) }}
-
{{ i18n.t('recentNDays', { n: 90 }) }}
+
{{ i18n.tsx.recentNDays({ n: 90 }) }}
@@ -307,7 +307,7 @@ async function resetPassword() { }); os.alert({ type: 'success', - text: i18n.t('newPasswordIs', { password }), + text: i18n.tsx.newPasswordIs({ password }), }); } } @@ -390,7 +390,7 @@ async function deleteAccount() { if (confirm.canceled) return; const typed = await os.inputText({ - text: i18n.t('typeToConfirm', { x: user.value?.username }), + text: i18n.tsx.typeToConfirm({ x: user.value?.username }), }); if (typed.canceled) return; diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue index eb9aef0e48..fe55fe3a02 100644 --- a/packages/frontend/src/pages/admin/ads.vue +++ b/packages/frontend/src/pages/admin/ads.vue @@ -160,7 +160,7 @@ function add() { function remove(ad) { os.confirm({ type: 'warning', - text: i18n.t('removeAreYouSure', { x: ad.url }), + text: i18n.tsx.removeAreYouSure({ x: ad.url }), }).then(({ canceled }) => { if (canceled) return; ads.value = ads.value.filter(x => x !== ad); diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index f941d512b3..44552fb88c 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._announcement.needConfirmationToRead }} -

{{ i18n.t('nUsersRead', { n: announcement.reads }) }}

+

{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}

{{ i18n.ts.save }} {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }}) @@ -109,7 +109,7 @@ function add() { function del(announcement) { os.confirm({ type: 'warning', - text: i18n.t('deleteAreYouSure', { x: announcement.title }), + text: i18n.tsx.deleteAreYouSure({ x: announcement.title }), }).then(({ canceled }) => { if (canceled) return; announcements.value = announcements.value.filter(x => x !== announcement); diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index 72b47949e7..dbbb3941d8 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -30,10 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue index 6811a8eba5..847c8bc1d4 100644 --- a/packages/frontend/src/pages/admin/relays.vue +++ b/packages/frontend/src/pages/admin/relays.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only - {{ i18n.t(`_relayStatus.${relay.status}`) }} + {{ i18n.ts._relayStatus[relay.status] }}
{{ i18n.ts.remove }}
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue index ff29f4ec1f..ad58255576 100644 --- a/packages/frontend/src/pages/admin/roles.role.vue +++ b/packages/frontend/src/pages/admin/roles.role.vue @@ -104,7 +104,7 @@ function edit() { async function del() { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('deleteAreYouSure', { x: role.name }), + text: i18n.tsx.deleteAreYouSure({ x: role.name }), }); if (canceled) return; diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index c31c6d0903..e3c0ea574a 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -78,7 +78,7 @@ async function read(announcement) { const confirm = await os.confirm({ type: 'question', title: i18n.ts._announcement.readConfirmTitle, - text: i18n.t('_announcement.readConfirmText', { title: announcement.title }), + text: i18n.tsx._announcement.readConfirmText({ title: announcement.title }), }); if (confirm.canceled) return; } diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue index 39a7924f94..50fd696af3 100644 --- a/packages/frontend/src/pages/auth.form.vue +++ b/packages/frontend/src/pages/auth.form.vue @@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + + + -
+
@@ -110,12 +136,16 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + + + -
+
@@ -126,12 +156,16 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + + + -
+
@@ -173,17 +207,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
- @@ -195,7 +222,6 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkInfo from '@/components/MkInfo.vue'; -import FormSection from '@/components/form/section.vue'; import FormSplit from '@/components/form/split.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; @@ -258,8 +284,8 @@ async function init(): Promise { urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl; } -async function save() { - await os.apiWithDialog('admin/update-meta', { +function saveInfo() { + os.apiWithDialog('admin/update-meta', { name: name.value, shortName: shortName.value === '' ? null : shortName.value, description: description.value, @@ -270,22 +296,57 @@ async function save() { inquiryUrl: inquiryUrl.value, repositoryUrl: repositoryUrl.value, impressumUrl: impressumUrl.value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_pinnedUsers() { + os.apiWithDialog('admin/update-meta', { pinnedUsers: pinnedUsers.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function saveFiles() { + os.apiWithDialog('admin/update-meta', { cacheRemoteFiles: cacheRemoteFiles.value, cacheRemoteSensitiveFiles: cacheRemoteSensitiveFiles.value, + }).then(() => { + fetchInstance(true); + }); +} + +function saveServiceWorker() { + os.apiWithDialog('admin/update-meta', { enableServiceWorker: enableServiceWorker.value, swPublicKey: swPublicKey.value, swPrivateKey: swPrivateKey.value, + }).then(() => { + fetchInstance(true); + }); +} + +function saveAd() { + os.apiWithDialog('admin/update-meta', { notesPerOneAd: notesPerOneAd.value, + }).then(() => { + fetchInstance(true); + }); +} + +function saveUrlPreview() { + os.apiWithDialog('admin/update-meta', { urlPreviewEnabled: urlPreviewEnabled.value, urlPreviewTimeout: urlPreviewTimeout.value, urlPreviewMaximumContentLength: urlPreviewMaximumContentLength.value, urlPreviewRequireContentLength: urlPreviewRequireContentLength.value, urlPreviewUserAgent: urlPreviewUserAgent.value, urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value, + }).then(() => { + fetchInstance(true); }); - - fetchInstance(true); } const headerTabs = computed(() => []); @@ -297,11 +358,6 @@ definePageMetadata(() => ({ diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue new file mode 100644 index 0000000000..1e88d59d8e --- /dev/null +++ b/packages/frontend/src/components/MkFormFooter.vue @@ -0,0 +1,49 @@ + + + + + + + diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index 0f4d94aa4e..57f68a2a26 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -7,103 +7,100 @@ SPDX-License-Identifier: AGPL-3.0-only - -
-
- - - - -
- -
- - - - -
- -
- - - - -
- -
- - - - -
- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - -
- - - - -
-
+
+
+ + + +
- + +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+
+
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index 9bccee89a5..975a4a1265 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -7,119 +7,115 @@ SPDX-License-Identifier: AGPL-3.0-only - -
- - - - - - - - +
+ - - + + + + + + + + - - - - - - - +
+ {{ i18n.ts._sensitiveMediaDetection.description }} -
- {{ i18n.ts._sensitiveMediaDetection.description }} + + + + + + - - - - - - + + + + - - - - + + + + - - - - + + + + - - - - + +
+ - + + + + + - {{ i18n.ts.save }} -
-
+
+ {{ i18n.ts.activeEmailValidationDescription }} + + + + + + + + + + + + + + + + + + + + + +
+
- - - - + + + -
- {{ i18n.ts.activeEmailValidationDescription }} - - - - - - - - - - - - - - - - - - - - - - {{ i18n.ts.save }} -
-
+
+ + + +
+
- - + + + + + -
- - - - {{ i18n.ts.save }} -
-
- - - - - - -
- - - -
-
-
- +
+ + + +
+
+
@@ -131,83 +127,80 @@ import XHeader from './_header_.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkSwitch from '@/components/MkSwitch.vue'; -import FormSuspense from '@/components/form/suspense.vue'; import MkRange from '@/components/MkRange.vue'; import MkInput from '@/components/MkInput.vue'; -import MkButton from '@/components/MkButton.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useForm } from '@/scripts/use-form.js'; +import MkFormFooter from '@/components/MkFormFooter.vue'; -const enableHcaptcha = ref(false); -const enableMcaptcha = ref(false); -const enableRecaptcha = ref(false); -const enableTurnstile = ref(false); -const sensitiveMediaDetection = ref('none'); -const sensitiveMediaDetectionSensitivity = ref(0); -const setSensitiveFlagAutomatically = ref(false); -const enableSensitiveMediaDetectionForVideos = ref(false); -const enableIpLogging = ref(false); -const enableActiveEmailValidation = ref(false); -const enableVerifymailApi = ref(false); -const verifymailAuthKey = ref(null); -const enableTruemailApi = ref(false); -const truemailInstance = ref(null); -const truemailAuthKey = ref(null); -const bannedEmailDomains = ref(''); +const meta = await misskeyApi('admin/meta'); -async function init() { - const meta = await misskeyApi('admin/meta'); - enableHcaptcha.value = meta.enableHcaptcha; - enableMcaptcha.value = meta.enableMcaptcha; - enableRecaptcha.value = meta.enableRecaptcha; - enableTurnstile.value = meta.enableTurnstile; - sensitiveMediaDetection.value = meta.sensitiveMediaDetection; - sensitiveMediaDetectionSensitivity.value = - meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 : - meta.sensitiveMediaDetectionSensitivity === 'low' ? 1 : - meta.sensitiveMediaDetectionSensitivity === 'medium' ? 2 : - meta.sensitiveMediaDetectionSensitivity === 'high' ? 3 : - meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 4 : 0; - setSensitiveFlagAutomatically.value = meta.setSensitiveFlagAutomatically; - enableSensitiveMediaDetectionForVideos.value = meta.enableSensitiveMediaDetectionForVideos; - enableIpLogging.value = meta.enableIpLogging; - enableActiveEmailValidation.value = meta.enableActiveEmailValidation; - enableVerifymailApi.value = meta.enableVerifymailApi; - verifymailAuthKey.value = meta.verifymailAuthKey; - enableTruemailApi.value = meta.enableTruemailApi; - truemailInstance.value = meta.truemailInstance; - truemailAuthKey.value = meta.truemailAuthKey; - bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || ''; -} - -function save() { - os.apiWithDialog('admin/update-meta', { - sensitiveMediaDetection: sensitiveMediaDetection.value, +const sensitiveMediaDetectionForm = useForm({ + sensitiveMediaDetection: meta.sensitiveMediaDetection, + sensitiveMediaDetectionSensitivity: meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 : + meta.sensitiveMediaDetectionSensitivity === 'low' ? 1 : + meta.sensitiveMediaDetectionSensitivity === 'medium' ? 2 : + meta.sensitiveMediaDetectionSensitivity === 'high' ? 3 : + meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 4 : 0, + setSensitiveFlagAutomatically: meta.setSensitiveFlagAutomatically, + enableSensitiveMediaDetectionForVideos: meta.enableSensitiveMediaDetectionForVideos, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + sensitiveMediaDetection: state.sensitiveMediaDetection, sensitiveMediaDetectionSensitivity: - sensitiveMediaDetectionSensitivity.value === 0 ? 'veryLow' : - sensitiveMediaDetectionSensitivity.value === 1 ? 'low' : - sensitiveMediaDetectionSensitivity.value === 2 ? 'medium' : - sensitiveMediaDetectionSensitivity.value === 3 ? 'high' : - sensitiveMediaDetectionSensitivity.value === 4 ? 'veryHigh' : + state.sensitiveMediaDetectionSensitivity === 0 ? 'veryLow' : + state.sensitiveMediaDetectionSensitivity === 1 ? 'low' : + state.sensitiveMediaDetectionSensitivity === 2 ? 'medium' : + state.sensitiveMediaDetectionSensitivity === 3 ? 'high' : + state.sensitiveMediaDetectionSensitivity === 4 ? 'veryHigh' : 0, - setSensitiveFlagAutomatically: setSensitiveFlagAutomatically.value, - enableSensitiveMediaDetectionForVideos: enableSensitiveMediaDetectionForVideos.value, - enableIpLogging: enableIpLogging.value, - enableActiveEmailValidation: enableActiveEmailValidation.value, - enableVerifymailApi: enableVerifymailApi.value, - verifymailAuthKey: verifymailAuthKey.value, - enableTruemailApi: enableTruemailApi.value, - truemailInstance: truemailInstance.value, - truemailAuthKey: truemailAuthKey.value, - bannedEmailDomains: bannedEmailDomains.value.split('\n'), - }).then(() => { - fetchInstance(true); + setSensitiveFlagAutomatically: state.setSensitiveFlagAutomatically, + enableSensitiveMediaDetectionForVideos: state.enableSensitiveMediaDetectionForVideos, }); -} + fetchInstance(true); +}); + +const ipLoggingForm = useForm({ + enableIpLogging: meta.enableIpLogging, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableIpLogging: state.enableIpLogging, + }); + fetchInstance(true); +}); + +const emailValidationForm = useForm({ + enableActiveEmailValidation: meta.enableActiveEmailValidation, + enableVerifymailApi: meta.enableVerifymailApi, + verifymailAuthKey: meta.verifymailAuthKey, + enableTruemailApi: meta.enableTruemailApi, + truemailInstance: meta.truemailInstance, + truemailAuthKey: meta.truemailAuthKey, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableActiveEmailValidation: state.enableActiveEmailValidation, + enableVerifymailApi: state.enableVerifymailApi, + verifymailAuthKey: state.verifymailAuthKey, + enableTruemailApi: state.enableTruemailApi, + truemailInstance: state.truemailInstance, + truemailAuthKey: state.truemailAuthKey, + }); + fetchInstance(true); +}); + +const bannedEmailDomainsForm = useForm({ + bannedEmailDomains: meta.bannedEmailDomains?.join('\n') || '', +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + bannedEmailDomains: state.bannedEmailDomains.split('\n'), + }); + fetchInstance(true); +}); const headerActions = computed(() => []); From 3f0aaaa41efe42776d70490ea213e3c8b194c152 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:49:52 +0900 Subject: [PATCH 0857/1120] perf(embed): improve embed performance (#14613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * refactor * refactor --------- Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> --- .../src/server/web/ClientServerService.ts | 66 +++++++++++++++++++ .../src/server/web/views/base-embed.pug | 3 + packages/frontend-embed/src/boot.ts | 10 ++- .../src/components/EmNoteDetailed.vue | 4 +- .../frontend-embed/src/components/EmNotes.vue | 6 +- packages/frontend-embed/src/di.ts | 2 + packages/frontend-embed/src/pages/clip.vue | 44 +++++++------ packages/frontend-embed/src/pages/note.vue | 35 +++++----- packages/frontend-embed/src/pages/tag.vue | 7 +- .../src/pages/user-timeline.vue | 49 ++++++++------ packages/frontend-embed/src/server-context.ts | 21 ++++++ packages/frontend-embed/src/ui.vue | 16 +++-- 12 files changed, 190 insertions(+), 73 deletions(-) create mode 100644 packages/frontend-embed/src/server-context.ts diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 063141273a..5de1f87667 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -785,6 +785,72 @@ export class ClientServerService { //#endregion //#region embed pages + fastify.get<{ Params: { user: string; } }>('/embed/user-timeline/:user', async (request, reply) => { + reply.removeHeader('X-Frame-Options'); + + const user = await this.usersRepository.findOneBy({ + id: request.params.user, + }); + + if (user == null) return; + if (user.host != null) return; + + const _user = await this.userEntityService.pack(user); + + reply.header('Cache-Control', 'public, max-age=3600'); + return await reply.view('base-embed', { + title: this.meta.name ?? 'Misskey', + ...await this.generateCommonPugData(this.meta), + embedCtx: htmlSafeJsonStringify({ + user: _user, + }), + }); + }); + + fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => { + reply.removeHeader('X-Frame-Options'); + + const note = await this.notesRepository.findOneBy({ + id: request.params.note, + }); + + if (note == null) return; + if (note.visibility !== 'public') return; + if (note.userHost != null) return; + + const _note = await this.noteEntityService.pack(note, null, { detail: true }); + + reply.header('Cache-Control', 'public, max-age=3600'); + return await reply.view('base-embed', { + title: this.meta.name ?? 'Misskey', + ...await this.generateCommonPugData(this.meta), + embedCtx: htmlSafeJsonStringify({ + note: _note, + }), + }); + }); + + fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => { + reply.removeHeader('X-Frame-Options'); + + const clip = await this.clipsRepository.findOneBy({ + id: request.params.clip, + }); + + if (clip == null) return; + + const _clip = await this.clipEntityService.pack(clip); + + reply.header('Cache-Control', 'public, max-age=3600'); + return await reply.view('base-embed', { + title: this.meta.name ?? 'Misskey', + ...await this.generateCommonPugData(this.meta), + embedCtx: htmlSafeJsonStringify({ + clip: _clip, + }), + }); + }); + fastify.get('/embed/*', async (request, reply) => { reply.removeHeader('X-Frame-Options'); diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug index d773f2676a..2bab20a36c 100644 --- a/packages/backend/src/server/web/views/base-embed.pug +++ b/packages/backend/src/server/web/views/base-embed.pug @@ -43,6 +43,9 @@ html(class='embed') script(type='application/json' id='misskey_meta' data-generated-at=now) != metaJson + script(type='application/json' id='misskey_embedCtx' data-generated-at=now) + != embedCtx + script include ../boot.embed.js diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts index fcea7d32ea..00c7944eb3 100644 --- a/packages/frontend-embed/src/boot.ts +++ b/packages/frontend-embed/src/boot.ts @@ -20,16 +20,19 @@ import { serverMetadata } from '@/server-metadata.js'; import { url } from '@@/js/config.js'; import { parseEmbedParams } from '@@/js/embed-page.js'; import { postMessageToParentWindow, setIframeId } from '@/post-message.js'; +import { serverContext } from '@/server-context.js'; import type { Theme } from '@/theme.js'; console.log('Misskey Embed'); +//#region Embedパラメータの取得・パース const params = new URLSearchParams(location.search); const embedParams = parseEmbedParams(params); - if (_DEV_) console.log(embedParams); +//#endregion +//#region テーマ function parseThemeOrNull(theme: string | null): Theme | null { if (theme == null) return null; try { @@ -65,6 +68,7 @@ if (embedParams.colorMode === 'dark') { } }); } +//#endregion // サイズの制限 document.documentElement.style.maxWidth = '500px'; @@ -89,6 +93,10 @@ const app = createApp( app.provide(DI.mediaProxy, new MediaProxy(serverMetadata, url)); +app.provide(DI.serverMetadata, serverMetadata); + +app.provide(DI.serverContext, serverContext); + app.provide(DI.embedParams, embedParams); // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue index 8169f500a9..a233011af7 100644 --- a/packages/frontend-embed/src/components/EmNoteDetailed.vue +++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue @@ -142,8 +142,8 @@ import EmAcct from '@/components/EmAcct.vue'; import { userPage } from '@/utils.js'; import { notePage } from '@/utils.js'; import { i18n } from '@/i18n.js'; +import { DI } from '@/di.js'; import { shouldCollapsed } from '@@/js/collapsed.js'; -import { serverMetadata } from '@/server-metadata.js'; import { url } from '@@/js/config.js'; import EmMfm from '@/components/EmMfm.js'; @@ -151,6 +151,8 @@ const props = defineProps<{ note: Misskey.entities.Note; }>(); +const serverMetadata = inject(DI.serverMetadata)!; + const inChannel = inject('inChannel', null); const note = ref(props.note); diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue index 6370f4aeae..3418d97f77 100644 --- a/packages/frontend-embed/src/components/EmNotes.vue +++ b/packages/frontend-embed/src/components/EmNotes.vue @@ -20,12 +20,12 @@ SPDX-License-Identifier: AGPL-3.0-only From 1b2b95e199938d30be546c1afa1088eecdc1097c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Sep 2024 01:22:57 +0000 Subject: [PATCH 0870/1120] Bump version to 2024.9.0-alpha.8 --- package.json | 2 +- packages/misskey-js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3bfa95a206..63c22cdc5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.9.0-alpha.7", + "version": "2024.9.0-alpha.8", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 6c07ce87da..0f797e8259 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.9.0-alpha.7", + "version": "2024.9.0-alpha.8", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", From 4be307f22363ab594984c240a292509bfb6895fa Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:55:35 +0900 Subject: [PATCH 0871/1120] refactor --- packages/frontend/src/ui/_common_/navbar-for-mobile.vue | 2 +- packages/frontend/src/ui/_common_/navbar.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index e80d5fd399..5115d21d56 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -82,7 +82,7 @@ function more() { diff --git a/idea/README.md b/idea/README.md new file mode 100644 index 0000000000..f64d16800a --- /dev/null +++ b/idea/README.md @@ -0,0 +1 @@ +使われなくなったけど消すのは勿体ない(将来使えるかもしれない)コードを入れておくとこ diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index 57f68a2a26..7e0a932f82 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
- - - - +
@@ -110,7 +112,6 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkInput from '@/components/MkInput.vue'; import MkLink from '@/components/MkLink.vue'; -import MkButton from '@/components/MkButton.vue'; import { useForm } from '@/scripts/use-form.js'; import MkFormFooter from '@/components/MkFormFooter.vue'; diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 537c86cb14..5207f0e38e 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -169,42 +169,44 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - +
@@ -230,7 +232,6 @@ SPDX-License-Identifier: AGPL-3.0-only From 8c3be57ab362b8b2a24dad9b42ac0c3762bcb34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:12:34 +0900 Subject: [PATCH 0890/1120] =?UTF-8?q?fix(frontend-embed):=20URL=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=82=B3=E3=83=BC=E3=83=89=E3=81=95=E3=82=8C=E3=81=9F?= =?UTF-8?q?=E6=96=87=E5=AD=97=E5=88=97=E3=81=8C=E6=AD=A3=E5=B8=B8=E3=81=AB?= =?UTF-8?q?=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=82=81=E3=81=AA=E3=81=84=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#14630)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend-embed): URLエンコードされた文字列が正常に読み込めない問題を修正 * fix(frontend-embed): bring back missing bits --- packages/frontend-embed/src/pages/user-timeline.vue | 13 ++++++++++++- packages/frontend-embed/src/ui.vue | 10 +++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue index 2d5dbb687b..85e6f52d50 100644 --- a/packages/frontend-embed/src/pages/user-timeline.vue +++ b/packages/frontend-embed/src/pages/user-timeline.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
+