Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
22
packages/backend/migration/1705475608437-reversi.js
Normal file
22
packages/backend/migration/1705475608437-reversi.js
Normal file
@@ -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") `);
|
||||
}
|
||||
}
|
18
packages/backend/migration/1705654039457-reversi-2.js
Normal file
18
packages/backend/migration/1705654039457-reversi-2.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Reversi21705654039457 {
|
||||
name = 'Reversi21705654039457'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Accepted" TO "user1Ready"`);
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Accepted" TO "user2Ready"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user1Ready" TO "user1Accepted"`);
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "user2Ready" TO "user2Accepted"`);
|
||||
}
|
||||
}
|
18
packages/backend/migration/1705793785675-reversi-3.js
Normal file
18
packages/backend/migration/1705793785675-reversi-3.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Reversi31705793785675 {
|
||||
name = 'Reversi31705793785675'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "surrendered" TO "surrenderedUserId"`);
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" ADD "timeoutUserId" character varying(32)`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "timeoutUserId"`);
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" RENAME COLUMN "surrenderedUserId" TO "surrendered"`);
|
||||
}
|
||||
}
|
18
packages/backend/migration/1705794768153-reversi-4.js
Normal file
18
packages/backend/migration/1705794768153-reversi-4.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Reversi41705794768153 {
|
||||
name = 'Reversi41705794768153'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" ADD "endedAt" TIMESTAMP WITH TIME ZONE`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."endedAt" IS 'The ended date of the ReversiGame.'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`COMMENT ON COLUMN "reversi_game"."endedAt" IS 'The ended date of the ReversiGame.'`);
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "endedAt"`);
|
||||
}
|
||||
}
|
16
packages/backend/migration/1705798904141-reversi-5.js
Normal file
16
packages/backend/migration/1705798904141-reversi-5.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Reversi51705798904141 {
|
||||
name = 'Reversi51705798904141'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" ADD "timeLimitForEachTurn" smallint NOT NULL DEFAULT '90'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "timeLimitForEachTurn"`);
|
||||
}
|
||||
}
|
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./built/boot/entry.js",
|
||||
"start:test": "NODE_ENV=test node ./built/boot/entry.js",
|
||||
"start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"migrate": "pnpm typeorm migration:run -d ormconfig.js",
|
||||
"revert": "pnpm typeorm migration:revert -d ormconfig.js",
|
||||
"check:connect": "node ./check_connect.js",
|
||||
@@ -31,24 +31,24 @@
|
||||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
|
||||
"generate-api-json": "node ./generate_api_json.js"
|
||||
"generate-api-json": "pnpm build && node ./generate_api_json.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@swc/core-darwin-arm64": "1.3.56",
|
||||
"@swc/core-darwin-x64": "1.3.56",
|
||||
"@swc/core-darwin-arm64": "1.3.105",
|
||||
"@swc/core-darwin-x64": "1.3.105",
|
||||
"@swc/core-freebsd-x64": "1.3.11",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.3.56",
|
||||
"@swc/core-linux-arm64-gnu": "1.3.56",
|
||||
"@swc/core-linux-arm64-musl": "1.3.56",
|
||||
"@swc/core-linux-x64-gnu": "1.3.56",
|
||||
"@swc/core-linux-x64-musl": "1.3.56",
|
||||
"@swc/core-win32-arm64-msvc": "1.3.56",
|
||||
"@swc/core-win32-ia32-msvc": "1.3.56",
|
||||
"@swc/core-win32-x64-msvc": "1.3.56",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.3.105",
|
||||
"@swc/core-linux-arm64-gnu": "1.3.105",
|
||||
"@swc/core-linux-arm64-musl": "1.3.105",
|
||||
"@swc/core-linux-x64-gnu": "1.3.105",
|
||||
"@swc/core-linux-x64-musl": "1.3.105",
|
||||
"@swc/core-win32-arm64-msvc": "1.3.105",
|
||||
"@swc/core-win32-ia32-msvc": "1.3.105",
|
||||
"@swc/core-win32-x64-msvc": "1.3.105",
|
||||
"@tensorflow/tfjs": "4.4.0",
|
||||
"@tensorflow/tfjs-node": "4.4.0",
|
||||
"bufferutil": "4.0.7",
|
||||
"bufferutil": "4.0.8",
|
||||
"slacc-android-arm-eabi": "0.0.10",
|
||||
"slacc-android-arm64": "0.0.10",
|
||||
"slacc-darwin-arm64": "0.0.10",
|
||||
@@ -67,16 +67,16 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.412.0",
|
||||
"@aws-sdk/lib-storage": "3.412.0",
|
||||
"@bull-board/api": "5.10.2",
|
||||
"@bull-board/fastify": "5.10.2",
|
||||
"@bull-board/ui": "5.10.2",
|
||||
"@bull-board/api": "5.13.0",
|
||||
"@bull-board/fastify": "5.13.0",
|
||||
"@bull-board/ui": "5.13.0",
|
||||
"@discordapp/twemoji": "15.0.2",
|
||||
"@fastify/accepts": "4.3.0",
|
||||
"@fastify/cookie": "9.2.0",
|
||||
"@fastify/cookie": "9.3.1",
|
||||
"@fastify/cors": "8.5.0",
|
||||
"@fastify/express": "2.3.0",
|
||||
"@fastify/http-proxy": "9.3.0",
|
||||
"@fastify/multipart": "8.0.0",
|
||||
"@fastify/multipart": "8.1.0",
|
||||
"@fastify/static": "6.12.0",
|
||||
"@fastify/view": "8.2.0",
|
||||
"@misskey-dev/sharp-read-bmp": "^1.1.1",
|
||||
@@ -85,20 +85,20 @@
|
||||
"@nestjs/core": "10.2.10",
|
||||
"@nestjs/testing": "10.2.10",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@simplewebauthn/server": "8.3.5",
|
||||
"@simplewebauthn/server": "9.0.0",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"@smithy/node-http-handler": "2.1.10",
|
||||
"@swc/cli": "0.1.63",
|
||||
"@swc/core": "1.3.100",
|
||||
"@swc/cli": "0.1.65",
|
||||
"@swc/core": "1.3.105",
|
||||
"@twemoji/parser": "15.0.0",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "6.0.1",
|
||||
"async-mutex": "0.4.0",
|
||||
"async-mutex": "0.4.1",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.2",
|
||||
"bullmq": "5.1.1",
|
||||
"bullmq": "5.1.4",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.1",
|
||||
"chalk": "5.3.0",
|
||||
@@ -107,12 +107,13 @@
|
||||
"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",
|
||||
"fastify": "4.25.2",
|
||||
"fastify-raw-body": "4.3.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "18.7.0",
|
||||
"file-type": "19.0.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "4.0.0",
|
||||
"got": "14.0.0",
|
||||
@@ -124,26 +125,27 @@
|
||||
"ipaddr.js": "2.1.0",
|
||||
"is-svg": "5.0.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "23.0.1",
|
||||
"jsdom": "23.2.0",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.3.2",
|
||||
"jsrsasign": "10.9.0",
|
||||
"meilisearch": "0.36.0",
|
||||
"jsrsasign": "11.0.0",
|
||||
"meilisearch": "0.37.0",
|
||||
"mfm-js": "0.24.0",
|
||||
"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",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.7",
|
||||
"nodemailer": "6.9.8",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "0.10.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.2.1",
|
||||
"otpauth": "9.2.2",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.11.3",
|
||||
"pino": "8.17.2",
|
||||
@@ -165,34 +167,33 @@
|
||||
"rxjs": "7.8.1",
|
||||
"sanitize-html": "2.11.0",
|
||||
"secure-json-parse": "2.7.0",
|
||||
"sharp": "0.32.6",
|
||||
"sharp": "0.33.2",
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.21.20",
|
||||
"systeminformation": "5.21.23",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.8",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typeorm": "0.3.17",
|
||||
"typeorm": "0.3.19",
|
||||
"typescript": "5.3.3",
|
||||
"ulid": "2.3.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.6",
|
||||
"ws": "8.15.1",
|
||||
"web-push": "3.6.7",
|
||||
"ws": "8.16.0",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@misskey-dev/eslint-plugin": "^1.0.0",
|
||||
"@nestjs/platform-express": "^10.3.0",
|
||||
"@simplewebauthn/typescript-types": "8.3.4",
|
||||
"@swc/jest": "0.2.29",
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@nestjs/platform-express": "10.3.0",
|
||||
"@simplewebauthn/types": "9.0.0",
|
||||
"@swc/jest": "0.2.31",
|
||||
"@types/accepts": "1.3.7",
|
||||
"@types/archiver": "6.0.2",
|
||||
"@types/bcryptjs": "2.4.6",
|
||||
"@types/body-parser": "1.19.5",
|
||||
"@types/cbor": "6.0.0",
|
||||
"@types/color-convert": "2.0.3",
|
||||
"@types/content-disposition": "0.5.8",
|
||||
"@types/fluent-ffmpeg": "2.1.24",
|
||||
@@ -204,8 +205,7 @@
|
||||
"@types/jsrsasign": "10.5.12",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/node": "20.11.5",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/oauth": "0.9.4",
|
||||
"@types/oauth2orize": "1.11.3",
|
||||
@@ -219,7 +219,6 @@
|
||||
"@types/rename": "1.0.7",
|
||||
"@types/sanitize-html": "2.9.5",
|
||||
"@types/semver": "7.5.6",
|
||||
"@types/sharp": "0.32.0",
|
||||
"@types/simple-oauth2": "5.0.7",
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
@@ -227,9 +226,9 @@
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.3",
|
||||
"@types/ws": "8.5.10",
|
||||
"@typescript-eslint/eslint-plugin": "6.14.0",
|
||||
"@typescript-eslint/parser": "6.14.0",
|
||||
"aws-sdk-client-mock": "3.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"aws-sdk-client-mock": "3.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
@@ -237,8 +236,8 @@
|
||||
"fkill": "^9.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"nodemon": "3.0.2",
|
||||
"pid-port": "^1.0.0",
|
||||
"nodemon": "3.0.3",
|
||||
"pid-port": "1.0.0",
|
||||
"simple-oauth2": "5.0.0"
|
||||
}
|
||||
}
|
||||
|
@@ -55,26 +55,32 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'antennaCreated':
|
||||
this.antennas.push({
|
||||
this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
lastUsedAt: new Date(body.lastUsedAt),
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
userList: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
break;
|
||||
case 'antennaUpdated': {
|
||||
const idx = this.antennas.findIndex(a => a.id === body.id);
|
||||
if (idx >= 0) {
|
||||
this.antennas[idx] = {
|
||||
this.antennas[idx] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
lastUsedAt: new Date(body.lastUsedAt),
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
userList: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
} else {
|
||||
// サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり
|
||||
this.antennas.push({
|
||||
this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
lastUsedAt: new Date(body.lastUsedAt),
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
userList: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -66,6 +66,7 @@ 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';
|
||||
@@ -113,6 +114,7 @@ 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 { ApAudienceService } from './activitypub/ApAudienceService.js';
|
||||
import { ApDbResolverService } from './activitypub/ApDbResolverService.js';
|
||||
import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js';
|
||||
@@ -200,6 +202,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 };
|
||||
@@ -249,6 +252,7 @@ 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 $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
||||
@@ -338,6 +342,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FanoutTimelineEndpointService,
|
||||
ChannelFollowingService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
@@ -385,6 +390,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
ApDeliverManagerService,
|
||||
@@ -469,6 +475,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FanoutTimelineEndpointService,
|
||||
$ChannelFollowingService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
@@ -516,6 +523,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FlashEntityService,
|
||||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
$ReversiGameEntityService,
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
$ApDeliverManagerService,
|
||||
@@ -601,6 +609,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FanoutTimelineEndpointService,
|
||||
ChannelFollowingService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
UsersChart,
|
||||
@@ -647,6 +656,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
ApDeliverManagerService,
|
||||
@@ -731,6 +741,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FanoutTimelineEndpointService,
|
||||
$ChannelFollowingService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
$UsersChart,
|
||||
@@ -777,6 +788,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FlashEntityService,
|
||||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
$ReversiGameEntityService,
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
$ApDeliverManagerService,
|
||||
|
@@ -165,10 +165,17 @@ export class EmailService {
|
||||
email: emailAddress,
|
||||
});
|
||||
|
||||
if (exist !== 0) {
|
||||
return {
|
||||
available: false,
|
||||
reason: 'used',
|
||||
};
|
||||
}
|
||||
|
||||
let validated: {
|
||||
valid: boolean,
|
||||
reason?: string | null,
|
||||
};
|
||||
} = { valid: true, reason: null };
|
||||
|
||||
if (meta.enableActiveEmailValidation) {
|
||||
validated = await validateEmail({
|
||||
@@ -187,27 +194,37 @@ export class EmailService {
|
||||
if (validated.valid && meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) {
|
||||
validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey);
|
||||
}
|
||||
} else {
|
||||
validated = { valid: true, reason: null };
|
||||
}
|
||||
|
||||
if (!validated.valid) {
|
||||
const formatReason: Record<string, 'format' | 'disposable' | 'mx' | 'smtp' | 'network' | 'blacklist' | undefined> = {
|
||||
regex: 'format',
|
||||
disposable: 'disposable',
|
||||
mx: 'mx',
|
||||
smtp: 'smtp',
|
||||
network: 'network',
|
||||
blacklist: 'blacklist',
|
||||
};
|
||||
|
||||
return {
|
||||
available: false,
|
||||
reason: validated.reason ? formatReason[validated.reason] ?? null : null,
|
||||
};
|
||||
}
|
||||
|
||||
const emailDomain: string = emailAddress.split('@')[1];
|
||||
const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain);
|
||||
|
||||
const available = exist === 0 && validated.valid && !isBanned;
|
||||
if (isBanned) {
|
||||
return {
|
||||
available: false,
|
||||
reason: 'banned',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
available,
|
||||
reason: available ? null :
|
||||
exist !== 0 ? 'used' :
|
||||
isBanned ? 'banned' :
|
||||
validated.reason === 'regex' ? 'format' :
|
||||
validated.reason === 'disposable' ? 'disposable' :
|
||||
validated.reason === 'mx' ? 'mx' :
|
||||
validated.reason === 'smtp' ? 'smtp' :
|
||||
validated.reason === 'network' ? 'network' :
|
||||
validated.reason === 'blacklist' ? 'blacklist' :
|
||||
null,
|
||||
available: true,
|
||||
reason: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -224,7 +241,8 @@ export class EmailService {
|
||||
},
|
||||
});
|
||||
|
||||
const json = (await res.json()) as {
|
||||
const json = (await res.json()) as Partial<{
|
||||
message: string;
|
||||
block: boolean;
|
||||
catch_all: boolean;
|
||||
deliverable_email: boolean;
|
||||
@@ -239,8 +257,15 @@ export class EmailService {
|
||||
mx_priority: { [key: string]: number };
|
||||
privacy: boolean;
|
||||
related_domains: string[];
|
||||
};
|
||||
}>;
|
||||
|
||||
/* api error: when there is only one `message` attribute in the returned result */
|
||||
if (Object.keys(json).length === 1 && Reflect.has(json, 'message')) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: null,
|
||||
};
|
||||
}
|
||||
if (json.email_address === undefined) {
|
||||
return {
|
||||
valid: false,
|
||||
@@ -283,13 +308,14 @@ export class EmailService {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
Authorization: truemailAuthKey
|
||||
Authorization: truemailAuthKey,
|
||||
},
|
||||
});
|
||||
|
||||
const json = (await res.json()) as {
|
||||
email: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
errors?: {
|
||||
list_match?: string;
|
||||
regex?: string;
|
||||
@@ -298,10 +324,10 @@ export class EmailService {
|
||||
} | null;
|
||||
};
|
||||
|
||||
if (json.email === undefined || (json.email !== undefined && json.errors?.regex)) {
|
||||
if (json.email === undefined || json.errors?.regex) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'format',
|
||||
valid: false,
|
||||
reason: 'format',
|
||||
};
|
||||
}
|
||||
if (json.errors?.smtp) {
|
||||
|
@@ -5,6 +5,7 @@
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import * as Reversi from 'misskey-reversi';
|
||||
import type { MiChannel } from '@/models/Channel.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
@@ -18,7 +19,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 +160,38 @@ export interface AdminEventTypes {
|
||||
comment: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReversiEventTypes {
|
||||
matched: {
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
};
|
||||
invited: {
|
||||
user: Packed<'User'>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReversiGameEventTypes {
|
||||
changeReadyStates: {
|
||||
user1: boolean;
|
||||
user2: boolean;
|
||||
};
|
||||
updateSettings: {
|
||||
userId: MiUser['id'];
|
||||
key: string;
|
||||
value: any;
|
||||
};
|
||||
log: Reversi.Serializer.Log & { id: string | null };
|
||||
started: {
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
};
|
||||
ended: {
|
||||
winnerId: MiUser['id'] | null;
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
};
|
||||
canceled: {
|
||||
userId: MiUser['id'];
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// 辞書(interface or type)から{ type, body }ユニオンを定義
|
||||
@@ -249,6 +282,14 @@ export type GlobalEvents = {
|
||||
name: 'notesStream';
|
||||
payload: Serialized<Packed<'Note'>>;
|
||||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<ReversiEventTypes>>;
|
||||
};
|
||||
reversiGame: {
|
||||
name: `reversiGameStream:${MiReversiGame['id']}`;
|
||||
payload: EventUnionFromDictionary<SerializedAll<ReversiGameEventTypes>>;
|
||||
};
|
||||
};
|
||||
|
||||
// API event definitions
|
||||
@@ -338,4 +379,14 @@ export class GlobalEventService {
|
||||
public publishAdminStream<K extends keyof AdminEventTypes>(userId: MiUser['id'], type: K, value?: AdminEventTypes[K]): void {
|
||||
this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishReversiStream<K extends keyof ReversiEventTypes>(userId: MiUser['id'], type: K, value?: ReversiEventTypes[K]): void {
|
||||
this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishReversiGameStream<K extends keyof ReversiGameEventTypes>(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void {
|
||||
this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
}
|
||||
|
@@ -51,7 +51,10 @@ export class MetaService implements OnApplicationShutdown {
|
||||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'metaUpdated': {
|
||||
this.cache = body;
|
||||
this.cache = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
proxyAccount: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
@@ -188,7 +188,7 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
const locale = locales[userProfile.lang ?? 'ja-JP'];
|
||||
const i18n = new I18n(locale);
|
||||
// TODO: render user information html
|
||||
sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
|
||||
sendEmail(userProfile.email, i18n.ts._email._follow.title, `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
const locale = locales[userProfile.lang ?? 'ja-JP'];
|
||||
const i18n = new I18n(locale);
|
||||
// TODO: render user information html
|
||||
sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
|
||||
sendEmail(userProfile.email, i18n.ts._email._receiveFollowRequest.title, `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
|
||||
*/
|
||||
}
|
||||
|
||||
|
555
packages/backend/src/core/ReversiService.ts
Normal file
555
packages/backend/src/core/ReversiService.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
/*
|
||||
* 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 CRC32 from 'crc-32';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import * as Reversi from 'misskey-reversi';
|
||||
import type {
|
||||
MiReversiGame,
|
||||
ReversiGamesRepository,
|
||||
} from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { Serialized } from '@/types.js';
|
||||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec
|
||||
|
||||
@Injectable()
|
||||
export class ReversiService implements OnApplicationShutdown, OnModuleInit {
|
||||
private notificationService: NotificationService;
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
this.notificationService = this.moduleRef.get(NotificationService.name);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async cacheGame(game: MiReversiGame) {
|
||||
await this.redisClient.setex(`reversi:game:cache:${game.id}`, 60 * 60, JSON.stringify(game));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async deleteGameCache(gameId: MiReversiGame['id']) {
|
||||
await this.redisClient.del(`reversi:game:cache:${gameId}`);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private getBakeProps(game: MiReversiGame) {
|
||||
return {
|
||||
startedAt: game.startedAt,
|
||||
endedAt: game.endedAt,
|
||||
// ゲームの途中からユーザーが変わることは無いので
|
||||
//user1Id: game.user1Id,
|
||||
//user2Id: game.user2Id,
|
||||
user1Ready: game.user1Ready,
|
||||
user2Ready: game.user2Ready,
|
||||
black: game.black,
|
||||
isStarted: game.isStarted,
|
||||
isEnded: game.isEnded,
|
||||
winnerId: game.winnerId,
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
timeLimitForEachTurn: game.timeLimitForEachTurn,
|
||||
logs: game.logs,
|
||||
map: game.map,
|
||||
bw: game.bw,
|
||||
crc32: game.crc32,
|
||||
} satisfies Partial<MiReversiGame>;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
|
||||
if (targetUser.id === me.id) {
|
||||
throw new Error('You cannot match yourself.');
|
||||
}
|
||||
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${me.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
|
||||
if (invitations.includes(targetUser.id)) {
|
||||
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id);
|
||||
|
||||
const game = await this.matched(targetUser.id, me.id);
|
||||
|
||||
return game;
|
||||
} else {
|
||||
this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
|
||||
|
||||
this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
|
||||
user: await this.userEntityService.pack(me, targetUser),
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async matchAnyUser(me: MiUser): Promise<MiReversiGame | null> {
|
||||
//#region まず自分宛ての招待を探す
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${me.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
|
||||
if (invitations.length > 0) {
|
||||
const invitorId = invitations[Math.floor(Math.random() * invitations.length)];
|
||||
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId);
|
||||
|
||||
const game = await this.matched(invitorId, me.id);
|
||||
|
||||
return game;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const matchings = await this.redisClient.zrange(
|
||||
'reversi:matchAny',
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
|
||||
const userIds = matchings.filter(id => id !== me.id);
|
||||
|
||||
if (userIds.length > 0) {
|
||||
// pick random
|
||||
const matchedUserId = userIds[Math.floor(Math.random() * userIds.length)];
|
||||
|
||||
await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId);
|
||||
|
||||
const game = await this.matched(matchedUserId, me.id);
|
||||
|
||||
return game;
|
||||
} else {
|
||||
await this.redisClient.zadd('reversi:matchAny', Date.now(), me.id);
|
||||
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.zrem('reversi:matchAny', user.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async gameReady(gameId: MiReversiGame['id'], user: MiUser, ready: boolean) {
|
||||
const game = await this.get(gameId);
|
||||
if (game == null) throw new Error('game not found');
|
||||
if (game.isStarted) return;
|
||||
|
||||
let isBothReady = false;
|
||||
|
||||
if (game.user1Id === user.id) {
|
||||
const updatedGame = {
|
||||
...game,
|
||||
user1Ready: ready,
|
||||
};
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
||||
user1: ready,
|
||||
user2: updatedGame.user2Ready,
|
||||
});
|
||||
|
||||
if (ready && updatedGame.user2Ready) isBothReady = true;
|
||||
} else if (game.user2Id === user.id) {
|
||||
const updatedGame = {
|
||||
...game,
|
||||
user2Ready: ready,
|
||||
};
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'changeReadyStates', {
|
||||
user1: updatedGame.user1Ready,
|
||||
user2: ready,
|
||||
});
|
||||
|
||||
if (ready && updatedGame.user1Ready) isBothReady = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBothReady) {
|
||||
// 3秒後、両者readyならゲーム開始
|
||||
setTimeout(async () => {
|
||||
const freshGame = await this.get(game.id);
|
||||
if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return;
|
||||
if (!freshGame.user1Ready || !freshGame.user2Ready) return;
|
||||
|
||||
this.startGame(freshGame);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async matched(parentId: MiUser['id'], childId: MiUser['id']): Promise<MiReversiGame> {
|
||||
const game = await this.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: parentId,
|
||||
user2Id: childId,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
logs: [],
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
}).then(x => this.reversiGamesRepository.findOneOrFail({
|
||||
where: { id: x.identifiers[0].id },
|
||||
relations: ['user1', 'user2'],
|
||||
}));
|
||||
this.cacheGame(game);
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game, null);
|
||||
this.globalEventService.publishReversiStream(parentId, 'matched', { game: packed });
|
||||
|
||||
return game;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async startGame(game: MiReversiGame) {
|
||||
let bw: number;
|
||||
if (game.bw === 'random') {
|
||||
bw = Math.random() > 0.5 ? 1 : 2;
|
||||
} else {
|
||||
bw = parseInt(game.bw, 10);
|
||||
}
|
||||
|
||||
const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
...this.getBakeProps(game),
|
||||
startedAt: new Date(),
|
||||
isStarted: true,
|
||||
black: bw,
|
||||
map: game.map,
|
||||
crc32,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
// キャッシュ効率化のためにユーザー情報は再利用
|
||||
updatedGame.user1 = game.user1;
|
||||
updatedGame.user2 = game.user2;
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
||||
const engine = new Reversi.Game(updatedGame.map, {
|
||||
isLlotheo: updatedGame.isLlotheo,
|
||||
canPutEverywhere: updatedGame.canPutEverywhere,
|
||||
loopedBoard: updatedGame.loopedBoard,
|
||||
});
|
||||
|
||||
if (engine.isEnded) {
|
||||
let winnerId;
|
||||
if (engine.winner === true) {
|
||||
winnerId = bw === 1 ? updatedGame.user1Id : updatedGame.user2Id;
|
||||
} else if (engine.winner === false) {
|
||||
winnerId = bw === 1 ? updatedGame.user2Id : updatedGame.user1Id;
|
||||
} else {
|
||||
winnerId = null;
|
||||
}
|
||||
|
||||
await this.endGame(updatedGame, winnerId, null);
|
||||
|
||||
return;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'started', {
|
||||
game: await this.reversiGameEntityService.packDetail(updatedGame, null),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async endGame(game: MiReversiGame, winnerId: MiUser['id'] | null, reason: 'surrender' | 'timeout' | null) {
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
...this.getBakeProps(game),
|
||||
isEnded: true,
|
||||
endedAt: new Date(),
|
||||
winnerId: winnerId,
|
||||
surrenderedUserId: reason === 'surrender' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null,
|
||||
timeoutUserId: reason === 'timeout' ? (winnerId === game.user1Id ? game.user2Id : game.user1Id) : null,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
// キャッシュ効率化のためにユーザー情報は再利用
|
||||
updatedGame.user1 = game.user1;
|
||||
updatedGame.user2 = game.user2;
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winnerId,
|
||||
game: await this.reversiGameEntityService.packDetail(updatedGame, null),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getInvitations(user: MiUser): Promise<MiUser['id'][]> {
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${user.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
return invitations;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateSettings(gameId: MiReversiGame['id'], user: MiUser, key: string, value: any) {
|
||||
const game = await this.get(gameId);
|
||||
if (game == null) throw new Error('game not found');
|
||||
if (game.isStarted) return;
|
||||
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
||||
if ((game.user1Id === user.id) && game.user1Ready) return;
|
||||
if ((game.user2Id === user.id) && game.user2Ready) return;
|
||||
|
||||
if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard', 'timeLimitForEachTurn'].includes(key)) return;
|
||||
|
||||
// TODO: より厳格なバリデーション
|
||||
|
||||
const updatedGame = {
|
||||
...game,
|
||||
[key]: value,
|
||||
};
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'updateSettings', {
|
||||
userId: user.id,
|
||||
key: key,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async putStoneToGame(gameId: MiReversiGame['id'], user: MiUser, pos: number, id?: string | null) {
|
||||
const game = await this.get(gameId);
|
||||
if (game == null) throw new Error('game not found');
|
||||
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 engine = Reversi.Serializer.restoreGame({
|
||||
map: game.map,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
logs: game.logs,
|
||||
});
|
||||
|
||||
if (engine.turn !== myColor) return;
|
||||
if (!engine.canPut(myColor, pos)) return;
|
||||
|
||||
engine.putStone(pos);
|
||||
|
||||
const logs = Reversi.Serializer.deserializeLogs(game.logs);
|
||||
|
||||
const log = {
|
||||
time: Date.now(),
|
||||
player: myColor,
|
||||
operation: 'put',
|
||||
pos,
|
||||
} as const;
|
||||
|
||||
logs.push(log);
|
||||
|
||||
const serializeLogs = Reversi.Serializer.serializeLogs(logs);
|
||||
|
||||
const crc32 = CRC32.str(JSON.stringify(serializeLogs)).toString();
|
||||
|
||||
const updatedGame = {
|
||||
...game,
|
||||
crc32,
|
||||
logs: serializeLogs,
|
||||
};
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'log', {
|
||||
...log,
|
||||
id: id ?? null,
|
||||
});
|
||||
|
||||
if (engine.isEnded) {
|
||||
let winnerId;
|
||||
if (engine.winner === true) {
|
||||
winnerId = game.black === 1 ? game.user1Id : game.user2Id;
|
||||
} else if (engine.winner === false) {
|
||||
winnerId = game.black === 1 ? game.user2Id : game.user1Id;
|
||||
} else {
|
||||
winnerId = null;
|
||||
}
|
||||
|
||||
await this.endGame(updatedGame, winnerId, null);
|
||||
} else {
|
||||
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`, updatedGame.timeLimitForEachTurn, '');
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async surrender(gameId: MiReversiGame['id'], user: MiUser) {
|
||||
const game = await this.get(gameId);
|
||||
if (game == null) throw new Error('game not found');
|
||||
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.endGame(game, winnerId, 'surrender');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async checkTimeout(gameId: MiReversiGame['id']) {
|
||||
const game = await this.get(gameId);
|
||||
if (game == null) throw new Error('game not found');
|
||||
if (game.isEnded) return;
|
||||
|
||||
const engine = Reversi.Serializer.restoreGame({
|
||||
map: game.map,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
logs: game.logs,
|
||||
});
|
||||
|
||||
if (engine.turn == null) return;
|
||||
|
||||
const timer = await this.redisClient.exists(`reversi:game:turnTimer:${game.id}:${engine.turn ? '1' : '0'}`);
|
||||
|
||||
if (timer === 0) {
|
||||
const winnerId = engine.turn ? (game.black === 1 ? game.user2Id : game.user1Id) : (game.black === 1 ? game.user1Id : game.user2Id);
|
||||
|
||||
await this.endGame(game, winnerId, 'timeout');
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async cancelGame(gameId: MiReversiGame['id'], user: MiUser) {
|
||||
const game = await this.get(gameId);
|
||||
if (game == null) throw new Error('game not found');
|
||||
if (game.isStarted) return;
|
||||
if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) return;
|
||||
|
||||
await this.reversiGamesRepository.delete(game.id);
|
||||
this.deleteGameCache(game.id);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'canceled', {
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async get(id: MiReversiGame['id']): Promise<MiReversiGame | null> {
|
||||
const cached = await this.redisClient.get(`reversi:game:cache:${id}`);
|
||||
if (cached != null) {
|
||||
// TODO: この辺りのデシリアライズ処理をどこか別のサービスに切り出したい
|
||||
const parsed = JSON.parse(cached) as Serialized<MiReversiGame>;
|
||||
return {
|
||||
...parsed,
|
||||
createdAt: new Date(parsed.createdAt),
|
||||
startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null,
|
||||
endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null,
|
||||
user1: parsed.user1 != null ? {
|
||||
...parsed.user1,
|
||||
avatar: null,
|
||||
banner: null,
|
||||
createdAt: new Date(parsed.user1.createdAt),
|
||||
updatedAt: parsed.user1.updatedAt != null ? new Date(parsed.user1.updatedAt) : null,
|
||||
lastActiveDate: parsed.user1.lastActiveDate != null ? new Date(parsed.user1.lastActiveDate) : null,
|
||||
lastFetchedAt: parsed.user1.lastFetchedAt != null ? new Date(parsed.user1.lastFetchedAt) : null,
|
||||
movedAt: parsed.user1.movedAt != null ? new Date(parsed.user1.movedAt) : null,
|
||||
} : null,
|
||||
user2: parsed.user2 != null ? {
|
||||
...parsed.user2,
|
||||
avatar: null,
|
||||
banner: null,
|
||||
createdAt: new Date(parsed.user2.createdAt),
|
||||
updatedAt: parsed.user2.updatedAt != null ? new Date(parsed.user2.updatedAt) : null,
|
||||
lastActiveDate: parsed.user2.lastActiveDate != null ? new Date(parsed.user2.lastActiveDate) : null,
|
||||
lastFetchedAt: parsed.user2.lastFetchedAt != null ? new Date(parsed.user2.lastFetchedAt) : null,
|
||||
movedAt: parsed.user2.movedAt != null ? new Date(parsed.user2.movedAt) : null,
|
||||
} : null,
|
||||
};
|
||||
} else {
|
||||
const game = await this.reversiGamesRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['user1', 'user2'],
|
||||
});
|
||||
if (game == null) return null;
|
||||
|
||||
this.cacheGame(game);
|
||||
|
||||
return game;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async checkCrc(gameId: MiReversiGame['id'], crc32: string | number) {
|
||||
const game = await this.get(gameId);
|
||||
if (game == null) throw new Error('game not found');
|
||||
|
||||
if (crc32.toString() !== game.crc32) {
|
||||
return await this.reversiGameEntityService.packDetail(game, null);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
@@ -185,10 +185,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
case 'userRoleAssigned': {
|
||||
const cached = this.roleAssignmentByUserIdCache.get(body.userId);
|
||||
if (cached) {
|
||||
cached.push({
|
||||
cached.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
role: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
@@ -29,7 +29,7 @@ import type {
|
||||
PublicKeyCredentialDescriptorFuture,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
RegistrationResponseJSON,
|
||||
} from '@simplewebauthn/typescript-types';
|
||||
} from '@simplewebauthn/types';
|
||||
|
||||
@Injectable()
|
||||
export class WebAuthnService {
|
||||
|
@@ -49,10 +49,11 @@ export class WebhookService implements OnApplicationShutdown {
|
||||
switch (type) {
|
||||
case 'webhookCreated':
|
||||
if (body.active) {
|
||||
this.webhooks.push({
|
||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -60,16 +61,18 @@ export class WebhookService implements OnApplicationShutdown {
|
||||
if (body.active) {
|
||||
const i = this.webhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
this.webhooks[i] = {
|
||||
this.webhooks[i] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
};
|
||||
} else {
|
||||
this.webhooks.push({
|
||||
this.webhooks.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
||||
...body,
|
||||
createdAt: new Date(body.createdAt),
|
||||
latestSentAt: body.latestSentAt ? new Date(body.latestSentAt) : null,
|
||||
user: null, // joinなカラムは通常取ってこないので
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
123
packages/backend/src/core/entities/ReversiGameEntityService.ts
Normal file
123
packages/backend/src/core/entities/ReversiGameEntityService.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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 { MiUser, 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 { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class ReversiGameEntityService {
|
||||
constructor(
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetail(
|
||||
src: MiReversiGame['id'] | MiReversiGame,
|
||||
me: { id: MiUser['id'] } | null | undefined,
|
||||
): Promise<Packed<'ReversiGameDetailed'>> {
|
||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const users = await this.userEntityService.packMany([
|
||||
game.user1 ?? game.user1Id,
|
||||
game.user2 ?? game.user2Id
|
||||
], me);
|
||||
|
||||
return await awaitAll({
|
||||
id: game.id,
|
||||
createdAt: game.createdAt.toISOString(),
|
||||
startedAt: game.startedAt && game.startedAt.toISOString(),
|
||||
endedAt: game.endedAt && game.endedAt.toISOString(),
|
||||
isStarted: game.isStarted,
|
||||
isEnded: game.isEnded,
|
||||
form1: game.form1,
|
||||
form2: game.form2,
|
||||
user1Ready: game.user1Ready,
|
||||
user2Ready: game.user2Ready,
|
||||
user1Id: game.user1Id,
|
||||
user2Id: game.user2Id,
|
||||
user1: users[0],
|
||||
user2: users[1],
|
||||
winnerId: game.winnerId,
|
||||
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
black: game.black,
|
||||
bw: game.bw,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
timeLimitForEachTurn: game.timeLimitForEachTurn,
|
||||
logs: game.logs,
|
||||
map: game.map,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailMany(
|
||||
xs: (MiReversiGame['id'] | MiReversiGame)[],
|
||||
me: { id: MiUser['id'] } | null | undefined,
|
||||
) : Promise<Packed<'ReversiGameDetailed'>[]> {
|
||||
return (await Promise.allSettled(xs.map(x => this.packDetail(x, me))))
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.map(result => (result as PromiseFulfilledResult<Packed<'ReversiGameDetailed'>>).value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiReversiGame['id'] | MiReversiGame,
|
||||
me: { id: MiUser['id'] } | null | undefined,
|
||||
): Promise<Packed<'ReversiGameLite'>> {
|
||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const users = await this.userEntityService.packMany([
|
||||
game.user1 ?? game.user1Id,
|
||||
game.user2 ?? game.user2Id
|
||||
], me);
|
||||
|
||||
return await awaitAll({
|
||||
id: game.id,
|
||||
createdAt: game.createdAt.toISOString(),
|
||||
startedAt: game.startedAt && game.startedAt.toISOString(),
|
||||
endedAt: game.endedAt && game.endedAt.toISOString(),
|
||||
isStarted: game.isStarted,
|
||||
isEnded: game.isEnded,
|
||||
user1Id: game.user1Id,
|
||||
user2Id: game.user2Id,
|
||||
user1: users[0],
|
||||
user2: users[1],
|
||||
winnerId: game.winnerId,
|
||||
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
black: game.black,
|
||||
bw: game.bw,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
timeLimitForEachTurn: game.timeLimitForEachTurn,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLiteMany(
|
||||
xs: (MiReversiGame['id'] | MiReversiGame)[],
|
||||
me: { id: MiUser['id'] } | null | undefined,
|
||||
) : Promise<Packed<'ReversiGameLite'>[]> {
|
||||
return (await Promise.allSettled(xs.map(x => this.packLite(x, me))))
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.map(result => (result as PromiseFulfilledResult<Packed<'ReversiGameLite'>>).value);
|
||||
}
|
||||
}
|
@@ -80,5 +80,6 @@ export const DI = {
|
||||
flashLikesRepository: Symbol('flashLikesRepository'),
|
||||
userMemosRepository: Symbol('userMemosRepository'),
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
@@ -41,6 +41,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 { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
@@ -86,6 +87,8 @@ export const refs = {
|
||||
Signin: packedSigninSchema,
|
||||
RoleLite: packedRoleLiteSchema,
|
||||
Role: packedRoleSchema,
|
||||
ReversiGameLite: packedReversiGameLiteSchema,
|
||||
ReversiGameDetailed: packedReversiGameDetailedSchema,
|
||||
AbuseUserReport: packedAbuseUserReportSchema,
|
||||
ModerationLog: packedModerationLogSchema,
|
||||
};
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiAbuseReportResolver, 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 { MiAbuseReportResolver, 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';
|
||||
|
||||
@@ -399,12 +399,18 @@ 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 $abuseReportResolversRepository: Provider = {
|
||||
provide: DI.abuseReportResolversRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiAbuseReportResolver),
|
||||
@@ -481,6 +487,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$abuseReportResolversRepository,
|
||||
],
|
||||
exports: [
|
||||
@@ -550,6 +557,7 @@ const $abuseReportResolversRepository: Provider = {
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
$abuseReportResolversRepository,
|
||||
],
|
||||
})
|
||||
|
145
packages/backend/src/models/ReversiGame.ts
Normal file
145
packages/backend/src/models/ReversiGame.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
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;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the ReversiGame.',
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
comment: 'The started date of the ReversiGame.',
|
||||
})
|
||||
public startedAt: Date | null;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
comment: 'The ended date of the ReversiGame.',
|
||||
})
|
||||
public endedAt: 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 user1Ready: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public user2Ready: 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 surrenderedUserId: MiUser['id'] | null;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public timeoutUserId: MiUser['id'] | null;
|
||||
|
||||
// in sec
|
||||
@Column('smallint', {
|
||||
default: 90,
|
||||
})
|
||||
public timeLimitForEachTurn: number;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public logs: 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;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32, nullable: true,
|
||||
})
|
||||
public crc32: string | null;
|
||||
}
|
@@ -70,6 +70,7 @@ 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 type { Repository } from 'typeorm';
|
||||
|
||||
export {
|
||||
@@ -140,6 +141,7 @@ export {
|
||||
MiFlashLike,
|
||||
MiUserMemo,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
};
|
||||
|
||||
export type AbuseReportResolversRepository = Repository<MiAbuseReportResolver>;
|
||||
@@ -209,3 +211,4 @@ export type FlashsRepository = Repository<MiFlash>;
|
||||
export type FlashLikesRepository = Repository<MiFlashLike>;
|
||||
export type UserMemoRepository = Repository<MiUserMemo>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>;
|
||||
export type ReversiGamesRepository = Repository<MiReversiGame>;
|
||||
|
232
packages/backend/src/models/json-schema/reversi-game.ts
Normal file
232
packages/backend/src/models/json-schema/reversi-game.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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',
|
||||
},
|
||||
endedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
isStarted: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isEnded: {
|
||||
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',
|
||||
},
|
||||
surrenderedUserId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
timeoutUserId: {
|
||||
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,
|
||||
},
|
||||
timeLimitForEachTurn: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedReversiGameDetailedSchema = {
|
||||
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',
|
||||
},
|
||||
endedAt: {
|
||||
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: 'object',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
form2: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
user1Ready: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user2Ready: {
|
||||
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',
|
||||
},
|
||||
surrenderedUserId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'id',
|
||||
},
|
||||
timeoutUserId: {
|
||||
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,
|
||||
},
|
||||
timeLimitForEachTurn: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
logs: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
map: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
@@ -78,6 +78,7 @@ 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 { Config } from '@/config.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -197,6 +198,7 @@ export const entities = [
|
||||
MiFlashLike,
|
||||
MiUserMemo,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
...charts,
|
||||
];
|
||||
|
||||
@@ -214,22 +216,24 @@ export function createPostgresDataSource(config: Config) {
|
||||
statement_timeout: 1000 * 10,
|
||||
...config.db.extra,
|
||||
},
|
||||
replication: config.dbReplications ? {
|
||||
master: {
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
username: config.db.user,
|
||||
password: config.db.pass,
|
||||
database: config.db.db,
|
||||
...(config.dbReplications ? {
|
||||
replication: {
|
||||
master: {
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
username: config.db.user,
|
||||
password: config.db.pass,
|
||||
database: config.db.db,
|
||||
},
|
||||
slaves: config.dbSlaves!.map(rep => ({
|
||||
host: rep.host,
|
||||
port: rep.port,
|
||||
username: rep.user,
|
||||
password: rep.pass,
|
||||
database: rep.db,
|
||||
})),
|
||||
},
|
||||
slaves: config.dbSlaves!.map(rep => ({
|
||||
host: rep.host,
|
||||
port: rep.port,
|
||||
username: rep.user,
|
||||
password: rep.pass,
|
||||
database: rep.db,
|
||||
})),
|
||||
} : undefined,
|
||||
} : {}),
|
||||
synchronize: process.env.NODE_ENV === 'test',
|
||||
dropSchema: process.env.NODE_ENV === 'test',
|
||||
cache: !config.db.disableCache && process.env.NODE_ENV !== 'test' ? { // dbをcloseしても何故かredisのコネクションが内部的に残り続けるようで、テストの際に支障が出るため無効にする(キャッシュも含めてテストしたいため本当は有効にしたいが...)
|
||||
|
@@ -664,6 +664,8 @@ export class ActivityPubServerService {
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
const userId = request.params.user;
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
@@ -676,6 +678,8 @@ export class ActivityPubServerService {
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
usernameLower: request.params.user.toLowerCase(),
|
||||
host: IsNull(),
|
||||
|
@@ -22,9 +22,12 @@ import { SigninApiService } from './api/SigninApiService.js';
|
||||
import { SigninService } from './api/SigninService.js';
|
||||
import { SignupApiService } from './api/SignupApiService.js';
|
||||
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
import { ClientServerService } from './web/ClientServerService.js';
|
||||
import { FeedService } from './web/FeedService.js';
|
||||
import { UrlPreviewService } from './web/UrlPreviewService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
import { MainChannelService } from './api/stream/channels/main.js';
|
||||
import { AdminChannelService } from './api/stream/channels/admin.js';
|
||||
import { AntennaChannelService } from './api/stream/channels/antenna.js';
|
||||
@@ -38,10 +41,9 @@ import { LocalTimelineChannelService } from './api/stream/channels/local-timelin
|
||||
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
|
||||
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
|
||||
import { UserListChannelService } from './api/stream/channels/user-list.js';
|
||||
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
|
||||
import { ClientLoggerService } from './web/ClientLoggerService.js';
|
||||
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
|
||||
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
import { ReversiChannelService } from './api/stream/channels/reversi.js';
|
||||
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -77,6 +79,8 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
||||
GlobalTimelineChannelService,
|
||||
HashtagChannelService,
|
||||
RoleTimelineChannelService,
|
||||
ReversiChannelService,
|
||||
ReversiGameChannelService,
|
||||
HomeTimelineChannelService,
|
||||
HybridTimelineChannelService,
|
||||
LocalTimelineChannelService,
|
||||
|
@@ -371,6 +371,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';
|
||||
@@ -740,6 +746,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: [
|
||||
@@ -1113,6 +1125,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,
|
||||
@@ -1477,6 +1495,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 {}
|
||||
|
@@ -22,7 +22,7 @@ import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
import { RateLimiterService } from './RateLimiterService.js';
|
||||
import { SigninService } from './SigninService.js';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/typescript-types';
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
|
@@ -372,6 +372,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],
|
||||
@@ -739,6 +745,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 {
|
||||
|
@@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // 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;
|
||||
});
|
||||
|
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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: 'read:account',
|
||||
|
||||
errors: {
|
||||
},
|
||||
|
||||
res: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private reversiService: ReversiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.userId) {
|
||||
await this.reversiService.matchSpecificUserCancel(me, ps.userId);
|
||||
return;
|
||||
} else {
|
||||
await this.reversiService.matchAnyUserCancel(me);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
63
packages/backend/src/server/api/endpoints/reversi/games.ts
Normal file
63
packages/backend/src/server/api/endpoints/reversi/games.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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: 'ReversiGameLite' },
|
||||
},
|
||||
} 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<typeof meta, typeof paramDef> { // 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')
|
||||
.innerJoinAndSelect('game.user1', 'user1')
|
||||
.innerJoinAndSelect('game.user2', 'user2');
|
||||
|
||||
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.packLiteMany(games, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: { ref: 'UserLite' },
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private userEntityService: UserEntityService,
|
||||
private reversiService: ReversiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const invitations = await this.reversiService.getInvitations(me);
|
||||
|
||||
return await this.userEntityService.packMany(invitations, me);
|
||||
});
|
||||
}
|
||||
}
|
66
packages/backend/src/server/api/endpoints/reversi/match.ts
Normal file
66
packages/backend/src/server/api/endpoints/reversi/match.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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';
|
||||
import { GetterService } from '../../GetterService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read: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', nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private getterService: GetterService,
|
||||
private reversiService: ReversiService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.userId === me.id) throw new ApiError(meta.errors.isYourself);
|
||||
|
||||
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 = target ? await this.reversiService.matchSpecificUser(me, target) : await this.reversiService.matchAnyUser(me);
|
||||
|
||||
if (game == null) return;
|
||||
|
||||
return await this.reversiGameEntityService.packDetail(game, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'ReversiGameDetailed',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
gameId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['gameId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
private reversiService: ReversiService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const game = await this.reversiService.get(ps.gameId);
|
||||
|
||||
if (game == null) {
|
||||
throw new ApiError(meta.errors.noSuchGame);
|
||||
}
|
||||
|
||||
return await this.reversiGameEntityService.packDetail(game, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -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: 'read: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<typeof meta, typeof paramDef> { // 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.id, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -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}`);
|
||||
|
125
packages/backend/src/server/api/stream/channels/reversi-game.ts
Normal file
125
packages/backend/src/server/api/stream/channels/reversi-game.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
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 {
|
||||
public readonly chName = 'reversiGame';
|
||||
public static shouldShare = false;
|
||||
public static requireCredential = false as const;
|
||||
private gameId: MiReversiGame['id'] | null = null;
|
||||
|
||||
constructor(
|
||||
private reversiService: ReversiService,
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
|
||||
id: string,
|
||||
connection: Channel['connection'],
|
||||
) {
|
||||
super(id, connection);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.gameId = params.gameId as string;
|
||||
|
||||
this.subscriber.on(`reversiGameStream:${this.gameId}`, this.send);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
switch (type) {
|
||||
case 'ready': this.ready(body); break;
|
||||
case 'updateSettings': this.updateSettings(body.key, body.value); break;
|
||||
case 'cancel': this.cancelGame(); break;
|
||||
case 'putStone': this.putStone(body.pos, body.id); break;
|
||||
case 'resync': this.resync(body.crc32); break;
|
||||
case 'claimTimeIsUp': this.claimTimeIsUp(); break;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async updateSettings(key: string, value: any) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.reversiService.updateSettings(this.gameId!, this.user, key, value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async ready(ready: boolean) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.reversiService.gameReady(this.gameId!, this.user, ready);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async cancelGame() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.reversiService.cancelGame(this.gameId!, this.user);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async putStone(pos: number, id: string) {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.reversiService.putStoneToGame(this.gameId!, this.user, pos, id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async resync(crc32: string | number) {
|
||||
const game = await this.reversiService.checkCrc(this.gameId!, crc32);
|
||||
if (game) {
|
||||
this.send('resynced', game);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async claimTimeIsUp() {
|
||||
if (this.user == null) return;
|
||||
|
||||
this.reversiService.checkTimeout(this.gameId!);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose() {
|
||||
// Unsubscribe events
|
||||
this.subscriber.off(`reversiGameStream:${this.gameId}`, this.send);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReversiGameChannelService implements MiChannelService<false> {
|
||||
public readonly shouldShare = ReversiGameChannel.shouldShare;
|
||||
public readonly requireCredential = ReversiGameChannel.requireCredential;
|
||||
public readonly kind = ReversiGameChannel.kind;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private reversiService: ReversiService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public create(id: string, connection: Channel['connection']): ReversiGameChannel {
|
||||
return new ReversiGameChannel(
|
||||
this.reversiService,
|
||||
this.reversiGamesRepository,
|
||||
this.reversiGameEntityService,
|
||||
id,
|
||||
connection,
|
||||
);
|
||||
}
|
||||
}
|
52
packages/backend/src/server/api/stream/channels/reversi.ts
Normal file
52
packages/backend/src/server/api/stream/channels/reversi.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bindThis } from '@/decorators.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(
|
||||
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 dispose() {
|
||||
// Unsubscribe events
|
||||
this.subscriber.off(`reversiStream:${this.user!.id}`, this.send);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReversiChannelService implements MiChannelService<true> {
|
||||
public readonly shouldShare = ReversiChannel.shouldShare;
|
||||
public readonly requireCredential = ReversiChannel.requireCredential;
|
||||
public readonly kind = ReversiChannel.kind;
|
||||
|
||||
constructor(
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public create(id: string, connection: Channel['connection']): ReversiChannel {
|
||||
return new ReversiChannel(
|
||||
id,
|
||||
connection,
|
||||
);
|
||||
}
|
||||
}
|
@@ -31,12 +31,13 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { FeedService } from './FeedService.js';
|
||||
import { UrlPreviewService } from './UrlPreviewService.js';
|
||||
@@ -84,6 +85,9 @@ export class ClientServerService {
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private flashEntityService: FlashEntityService,
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
@@ -91,6 +95,7 @@ export class ClientServerService {
|
||||
private galleryPostEntityService: GalleryPostEntityService,
|
||||
private clipEntityService: ClipEntityService,
|
||||
private channelEntityService: ChannelEntityService,
|
||||
private reversiGameEntityService: ReversiGameEntityService,
|
||||
private metaService: MetaService,
|
||||
private urlPreviewService: UrlPreviewService,
|
||||
private feedService: FeedService,
|
||||
@@ -490,6 +495,8 @@ export class ClientServerService {
|
||||
isSuspended: false,
|
||||
});
|
||||
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
if (user != null) {
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
|
||||
const meta = await this.metaService.fetch();
|
||||
@@ -529,6 +536,8 @@ export class ClientServerService {
|
||||
return;
|
||||
}
|
||||
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
reply.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`);
|
||||
});
|
||||
|
||||
@@ -731,6 +740,25 @@ export class ClientServerService {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
});
|
||||
|
||||
// Reversi game
|
||||
fastify.get<{ Params: { game: string; } }>('/reversi/g/:game', async (request, reply) => {
|
||||
const game = await this.reversiGamesRepository.findOneBy({
|
||||
id: request.params.game,
|
||||
});
|
||||
|
||||
if (game) {
|
||||
const _game = await this.reversiGameEntityService.packDetail(game, null);
|
||||
const meta = await this.metaService.fetch();
|
||||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await reply.view('reversi-game', {
|
||||
game: _game,
|
||||
...this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
fastify.get('/_info_card_', async (request, reply) => {
|
||||
|
20
packages/backend/src/server/web/views/reversi-game.pug
Normal file
20
packages/backend/src/server/web/views/reversi-game.pug
Normal file
@@ -0,0 +1,20 @@
|
||||
extends ./base
|
||||
|
||||
block vars
|
||||
- const user1 = game.user1;
|
||||
- const user2 = game.user2;
|
||||
- const title = `${user1.username} vs ${user2.username}`;
|
||||
- const url = `${config.url}/reversi/g/${game.id}`;
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
||||
block desc
|
||||
meta(name='description' content='⚫⚪Misskey Reversi⚪⚫')
|
||||
|
||||
block og
|
||||
meta(property='og:type' content='article')
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content='⚫⚪Misskey Reversi⚪⚫')
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='twitter:card' content='summary')
|
@@ -277,7 +277,11 @@ export type Serialized<T> = {
|
||||
? (string | null)
|
||||
: T[K] extends Record<string, any>
|
||||
? Serialized<T[K]>
|
||||
: T[K];
|
||||
: T[K] extends (Record<string, any> | null)
|
||||
? (Serialized<T[K]> | null)
|
||||
: T[K] extends (Record<string, any> | undefined)
|
||||
? (Serialized<T[K]> | undefined)
|
||||
: T[K];
|
||||
};
|
||||
|
||||
export type FilterUnionByProperty<
|
||||
|
@@ -18,7 +18,7 @@ import type {
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
RegistrationResponseJSON,
|
||||
} from '@simplewebauthn/typescript-types';
|
||||
} from '@simplewebauthn/types';
|
||||
import type * as misskey from 'misskey-js';
|
||||
|
||||
describe('2要素認証', () => {
|
||||
|
Reference in New Issue
Block a user