merge: upstream (1)
This commit is contained in:
commit
db012fc8c3
258 changed files with 18802 additions and 7557 deletions
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"`);
|
||||
}
|
||||
}
|
|
@ -70,11 +70,11 @@
|
|||
"@bull-board/ui": "5.10.2",
|
||||
"@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",
|
||||
|
@ -84,21 +84,21 @@
|
|||
"@nestjs/testing": "10.2.10",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sharkey/sfm-js": "0.24.4",
|
||||
"@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/core": "1.3.105",
|
||||
"@twemoji/parser": "15.0.0",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "6.0.1",
|
||||
"argon2": "^0.31.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": "4.15.4",
|
||||
"bullmq": "5.1.4",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.1",
|
||||
"chalk": "5.3.0",
|
||||
|
@ -107,13 +107,14 @@
|
|||
"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-multer": "^2.0.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",
|
||||
"glob": "10.3.10",
|
||||
|
@ -126,25 +127,26 @@
|
|||
"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",
|
||||
"megalodon": "workspace:*",
|
||||
"jsrsasign": "11.0.0",
|
||||
"meilisearch": "0.37.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",
|
||||
"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",
|
||||
"pkce-challenge": "4.0.1",
|
||||
|
@ -168,26 +170,26 @@
|
|||
"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",
|
||||
"uuid": "^9.0.1",
|
||||
"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",
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@nestjs/platform-express": "10.3.0",
|
||||
"@simplewebauthn/typescript-types": "8.3.4",
|
||||
"@swc/jest": "0.2.29",
|
||||
"@swc/jest": "0.2.31",
|
||||
"@types/accepts": "1.3.7",
|
||||
"@types/archiver": "6.0.2",
|
||||
"@types/bcryptjs": "2.4.6",
|
||||
|
@ -204,7 +206,7 @@
|
|||
"@types/jsrsasign": "10.5.12",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/node": "20.11.5",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/oauth": "0.9.4",
|
||||
|
@ -228,9 +230,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.19.0",
|
||||
"@typescript-eslint/parser": "6.19.0",
|
||||
"aws-sdk-client-mock": "3.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
|
@ -238,8 +240,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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,6 +66,8 @@ import { FeaturedService } from './FeaturedService.js';
|
|||
import { FanoutTimelineService } from './FanoutTimelineService.js';
|
||||
import { ChannelFollowingService } from './ChannelFollowingService.js';
|
||||
import { RegistryApiService } from './RegistryApiService.js';
|
||||
import { ReversiService } from './ReversiService.js';
|
||||
|
||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
||||
import FederationChart from './chart/charts/federation.js';
|
||||
import NotesChart from './chart/charts/notes.js';
|
||||
|
@ -80,6 +82,7 @@ import PerUserFollowingChart from './chart/charts/per-user-following.js';
|
|||
import PerUserDriveChart from './chart/charts/per-user-drive.js';
|
||||
import ApRequestChart from './chart/charts/ap-request.js';
|
||||
import { ChartManagementService } from './chart/ChartManagementService.js';
|
||||
|
||||
import { AbuseUserReportEntityService } from './entities/AbuseUserReportEntityService.js';
|
||||
import { AntennaEntityService } from './entities/AntennaEntityService.js';
|
||||
import { AppEntityService } from './entities/AppEntityService.js';
|
||||
|
@ -112,6 +115,8 @@ 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';
|
||||
|
@ -199,6 +204,7 @@ const $FanoutTimelineService: Provider = { provide: 'FanoutTimelineService', use
|
|||
const $FanoutTimelineEndpointService: Provider = { provide: 'FanoutTimelineEndpointService', useExisting: FanoutTimelineEndpointService };
|
||||
const $ChannelFollowingService: Provider = { provide: 'ChannelFollowingService', useExisting: ChannelFollowingService };
|
||||
const $RegistryApiService: Provider = { provide: 'RegistryApiService', useExisting: RegistryApiService };
|
||||
const $ReversiService: Provider = { provide: 'ReversiService', useExisting: ReversiService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||
|
@ -247,6 +253,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 };
|
||||
|
@ -336,6 +343,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
FanoutTimelineEndpointService,
|
||||
ChannelFollowingService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
|
@ -350,6 +359,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
PerUserDriveChart,
|
||||
ApRequestChart,
|
||||
ChartManagementService,
|
||||
|
||||
AbuseUserReportEntityService,
|
||||
AntennaEntityService,
|
||||
AppEntityService,
|
||||
|
@ -382,6 +392,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
ApDeliverManagerService,
|
||||
|
@ -466,6 +478,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$FanoutTimelineEndpointService,
|
||||
$ChannelFollowingService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
|
@ -480,6 +494,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$PerUserDriveChart,
|
||||
$ApRequestChart,
|
||||
$ChartManagementService,
|
||||
|
||||
$AbuseUserReportEntityService,
|
||||
$AntennaEntityService,
|
||||
$AppEntityService,
|
||||
|
@ -512,6 +527,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$FlashEntityService,
|
||||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
$ReversiGameEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
$ApDeliverManagerService,
|
||||
|
@ -597,6 +614,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
FanoutTimelineEndpointService,
|
||||
ChannelFollowingService,
|
||||
RegistryApiService,
|
||||
ReversiService,
|
||||
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
UsersChart,
|
||||
|
@ -610,6 +629,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
PerUserDriveChart,
|
||||
ApRequestChart,
|
||||
ChartManagementService,
|
||||
|
||||
AbuseUserReportEntityService,
|
||||
AntennaEntityService,
|
||||
AppEntityService,
|
||||
|
@ -642,6 +662,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
FlashEntityService,
|
||||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
ApDeliverManagerService,
|
||||
|
@ -726,6 +748,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$FanoutTimelineEndpointService,
|
||||
$ChannelFollowingService,
|
||||
$RegistryApiService,
|
||||
$ReversiService,
|
||||
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
$UsersChart,
|
||||
|
@ -739,6 +763,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$PerUserDriveChart,
|
||||
$ApRequestChart,
|
||||
$ChartManagementService,
|
||||
|
||||
$AbuseUserReportEntityService,
|
||||
$AntennaEntityService,
|
||||
$AppEntityService,
|
||||
|
@ -771,6 +796,8 @@ 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) {
|
||||
if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
|
||||
|
@ -185,27 +192,37 @@ export class EmailService {
|
|||
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
|
||||
});
|
||||
}
|
||||
} 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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -222,7 +239,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;
|
||||
|
@ -237,8 +255,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,
|
||||
|
@ -281,25 +306,26 @@ 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;
|
||||
errors?: {
|
||||
error?: string;
|
||||
errors?: {
|
||||
list_match?: string;
|
||||
regex?: string;
|
||||
mx?: string;
|
||||
smtp?: string;
|
||||
} | 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) {
|
||||
|
@ -320,7 +346,7 @@ export class EmailService {
|
|||
reason: json.errors?.list_match as T || 'blacklist',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
reason: null,
|
||||
|
|
|
@ -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';
|
||||
|
@ -162,6 +163,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 }ユニオンを定義
|
||||
|
@ -252,6 +285,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
|
||||
|
@ -341,4 +382,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);
|
||||
}
|
||||
}
|
||||
|
|
578
packages/backend/src/core/ReversiService.ts
Normal file
578
packages/backend/src/core/ReversiService.ts
Normal file
|
@ -0,0 +1,578 @@
|
|||
/*
|
||||
* 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 { IsNull } from 'typeorm';
|
||||
import type {
|
||||
MiReversiGame,
|
||||
ReversiGamesRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { Packed } from '@/misc/json-schema.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 * 3, JSON.stringify(game));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async deleteGameCache(gameId: MiReversiGame['id']) {
|
||||
await this.redisClient.del(`reversi:game:cache:${gameId}`);
|
||||
}
|
||||
|
||||
@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.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: targetUser.id,
|
||||
user2Id: me.id,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
logs: [],
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
this.cacheGame(game);
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: targetUser.id });
|
||||
this.globalEventService.publishReversiStream(targetUser.id, 'matched', { game: packed });
|
||||
|
||||
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.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: invitorId,
|
||||
user2Id: me.id,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
logs: [],
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
this.cacheGame(game);
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: invitorId });
|
||||
this.globalEventService.publishReversiStream(invitorId, 'matched', { game: packed });
|
||||
|
||||
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.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: matchedUserId,
|
||||
user2Id: me.id,
|
||||
user1Ready: false,
|
||||
user2Ready: false,
|
||||
isStarted: false,
|
||||
isEnded: false,
|
||||
logs: [],
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
}).then(x => this.reversiGamesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
this.cacheGame(game);
|
||||
|
||||
const packed = await this.reversiGameEntityService.packDetail(game, { id: matchedUserId });
|
||||
this.globalEventService.publishReversiStream(matchedUserId, 'matched', { game: packed });
|
||||
|
||||
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 = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
user1Ready: ready,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
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 = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
user2Ready: ready,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
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 startGame(game: MiReversiGame) {
|
||||
let bw: number;
|
||||
if (game.bw === 'random') {
|
||||
bw = Math.random() > 0.5 ? 1 : 2;
|
||||
} else {
|
||||
bw = parseInt(game.bw, 10);
|
||||
}
|
||||
|
||||
function getRandomMap() {
|
||||
const mapCount = Object.entries(Reversi.maps).length;
|
||||
const rnd = Math.floor(Math.random() * mapCount);
|
||||
return Object.values(Reversi.maps)[rnd].data;
|
||||
}
|
||||
|
||||
const map = game.map != null ? game.map : getRandomMap();
|
||||
|
||||
const crc32 = CRC32.str(JSON.stringify(game.logs)).toString();
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
startedAt: new Date(),
|
||||
isStarted: true,
|
||||
black: bw,
|
||||
map: map,
|
||||
crc32,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
//#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理
|
||||
const engine = new Reversi.Game(map, {
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
});
|
||||
|
||||
if (engine.isEnded) {
|
||||
let winner;
|
||||
if (engine.winner === true) {
|
||||
winner = bw === 1 ? game.user1Id : game.user2Id;
|
||||
} else if (engine.winner === false) {
|
||||
winner = bw === 1 ? game.user2Id : game.user1Id;
|
||||
} else {
|
||||
winner = null;
|
||||
}
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
isEnded: true,
|
||||
endedAt: new Date(),
|
||||
winnerId: winner,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winner,
|
||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
this.redisClient.setex(`reversi:game:turnTimer:${game.id}:1`, updatedGame.timeLimitForEachTurn, '');
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'started', {
|
||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||
});
|
||||
}
|
||||
|
||||
@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 = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
[key]: value,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
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);
|
||||
|
||||
let winner;
|
||||
if (engine.isEnded) {
|
||||
if (engine.winner === true) {
|
||||
winner = game.black === 1 ? game.user1Id : game.user2Id;
|
||||
} else if (engine.winner === false) {
|
||||
winner = game.black === 1 ? game.user2Id : game.user1Id;
|
||||
} else {
|
||||
winner = null;
|
||||
}
|
||||
}
|
||||
|
||||
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 = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
crc32,
|
||||
isEnded: engine.isEnded,
|
||||
winnerId: winner,
|
||||
logs: serializeLogs,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'log', {
|
||||
...log,
|
||||
id: id ?? null,
|
||||
});
|
||||
|
||||
if (engine.isEnded) {
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winner ?? null,
|
||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||
});
|
||||
} 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;
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
isEnded: true,
|
||||
endedAt: new Date(),
|
||||
winnerId: winnerId,
|
||||
surrenderedUserId: user.id,
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winnerId,
|
||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||
});
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
const updatedGame = await this.reversiGamesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
isEnded: true,
|
||||
endedAt: new Date(),
|
||||
winnerId: winnerId,
|
||||
timeoutUserId: engine.turn ? (game.black === 1 ? game.user1Id : game.user2Id) : (game.black === 1 ? game.user2Id : game.user1Id),
|
||||
})
|
||||
.where('id = :id', { id: game.id })
|
||||
.returning('*')
|
||||
.execute()
|
||||
.then((response) => response.raw[0]);
|
||||
this.cacheGame(updatedGame);
|
||||
|
||||
this.globalEventService.publishReversiGameStream(game.id, 'ended', {
|
||||
winnerId: winnerId,
|
||||
game: await this.reversiGameEntityService.packDetail(game.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
const parsed = JSON.parse(cached) as Serialized<MiReversiGame>;
|
||||
return {
|
||||
...parsed,
|
||||
startedAt: parsed.startedAt != null ? new Date(parsed.startedAt) : null,
|
||||
endedAt: parsed.endedAt != null ? new Date(parsed.endedAt) : null,
|
||||
};
|
||||
} else {
|
||||
const game = await this.reversiGamesRepository.findOneBy({ id });
|
||||
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);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
|
@ -58,7 +58,7 @@ export class ApAudienceService {
|
|||
};
|
||||
}
|
||||
|
||||
if (toGroups.followers.length > 0) {
|
||||
if (toGroups.followers.length > 0 || ccGroups.followers.length > 0) {
|
||||
return {
|
||||
visibility: 'followers',
|
||||
mentionedUsers,
|
||||
|
|
117
packages/backend/src/core/entities/ReversiGameEntityService.ts
Normal file
117
packages/backend/src/core/entities/ReversiGameEntityService.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { ReversiGamesRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class ReversiGameEntityService {
|
||||
constructor(
|
||||
@Inject(DI.reversiGamesRepository)
|
||||
private reversiGamesRepository: ReversiGamesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async 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 });
|
||||
|
||||
return await awaitAll({
|
||||
id: game.id,
|
||||
createdAt: this.idService.parse(game.id).date.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: this.userEntityService.pack(game.user1Id, me),
|
||||
user2: this.userEntityService.pack(game.user2Id, me),
|
||||
winnerId: game.winnerId,
|
||||
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
||||
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 packDetailMany(
|
||||
xs: MiReversiGame[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(xs.map(x => this.packDetail(x, me)));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiReversiGame['id'] | MiReversiGame,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
): Promise<Packed<'ReversiGameLite'>> {
|
||||
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: game.id,
|
||||
createdAt: this.idService.parse(game.id).date.toISOString(),
|
||||
startedAt: game.startedAt && game.startedAt.toISOString(),
|
||||
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: this.userEntityService.pack(game.user1Id, me),
|
||||
user2: this.userEntityService.pack(game.user2Id, me),
|
||||
winnerId: game.winnerId,
|
||||
winner: game.winnerId ? this.userEntityService.pack(game.winnerId, me) : null,
|
||||
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 packLiteMany(
|
||||
xs: MiReversiGame[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
return Promise.all(xs.map(x => this.packLite(x, me)));
|
||||
}
|
||||
}
|
||||
|
|
@ -80,5 +80,6 @@ export const DI = {
|
|||
userMemosRepository: Symbol('userMemosRepository'),
|
||||
noteEditRepository: Symbol('noteEditRepository'),
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
reversiGamesRepository: Symbol('reversiGamesRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
|
|
@ -39,6 +39,7 @@ import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
|||
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
||||
import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js';
|
||||
import { packedAdSchema } from '@/models/json-schema/ad.js';
|
||||
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
|
@ -78,6 +79,8 @@ export const refs = {
|
|||
Signin: packedSigninSchema,
|
||||
RoleLite: packedRoleLiteSchema,
|
||||
Role: packedRoleSchema,
|
||||
ReversiGameLite: packedReversiGameLiteSchema,
|
||||
ReversiGameDetailed: packedReversiGameDetailedSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit, MiBubbleGameRecord } from './_.js';
|
||||
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, NoteEdit, MiBubbleGameRecord, MiReversiGame } from './_.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
|
@ -411,6 +411,12 @@ const $bubbleGameRecordsRepository: Provider = {
|
|||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $reversiGamesRepository: Provider = {
|
||||
provide: DI.reversiGamesRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiReversiGame),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
],
|
||||
|
@ -482,6 +488,7 @@ const $bubbleGameRecordsRepository: Provider = {
|
|||
$userMemosRepository,
|
||||
$noteEditRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
|
@ -551,6 +558,7 @@ const $bubbleGameRecordsRepository: Provider = {
|
|||
$userMemosRepository,
|
||||
$noteEditRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
$reversiGamesRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {}
|
||||
|
|
138
packages/backend/src/models/ReversiGame.ts
Normal file
138
packages/backend/src/models/ReversiGame.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('reversi_game')
|
||||
export class MiReversiGame {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
comment: 'The started date of the ReversiGame.',
|
||||
})
|
||||
public startedAt: Date | null;
|
||||
|
||||
@Column('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,8 @@ import { MiFlashLike } from '@/models/FlashLike.js';
|
|||
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
|
||||
import { NoteEdit } from '@/models/NoteEdit.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
|
||||
import type { Repository } from 'typeorm';
|
||||
|
||||
export {
|
||||
|
@ -140,6 +142,7 @@ export {
|
|||
MiUserMemo,
|
||||
NoteEdit,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>;
|
||||
|
@ -209,3 +212,4 @@ export type FlashLikesRepository = Repository<MiFlashLike>;
|
|||
export type UserMemoRepository = Repository<MiUserMemo>;
|
||||
export type NoteEditRepository = Repository<NoteEdit>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>;
|
||||
export type ReversiGamesRepository = Repository<MiReversiGame>;
|
||||
|
|
248
packages/backend/src/models/json-schema/reversi-game.ts
Normal file
248
packages/backend/src/models/json-schema/reversi-game.ts
Normal file
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
* 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,
|
||||
},
|
||||
form1: {
|
||||
type: 'any',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
form2: {
|
||||
type: 'any',
|
||||
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,
|
||||
},
|
||||
},
|
||||
} 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: 'any',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
form2: {
|
||||
type: 'any',
|
||||
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 { MiFlashLike } from '@/models/FlashLike.js';
|
|||
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||
import { NoteEdit } from '@/models/NoteEdit.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiReversiGame } from '@/models/ReversiGame.js';
|
||||
|
||||
import { Config } from '@/config.js';
|
||||
import MisskeyLogger from '@/logger.js';
|
||||
|
@ -194,6 +195,7 @@ export const entities = [
|
|||
MiUserMemo,
|
||||
NoteEdit,
|
||||
MiBubbleGameRecord,
|
||||
MiReversiGame,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
@ -211,22 +213,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のコネクションが内部的に残り続けるようで、テストの際に支障が出るため無効にする(キャッシュも含めてテストしたいため本当は有効にしたいが...)
|
||||
|
|
|
@ -795,6 +795,8 @@ export class ActivityPubServerService {
|
|||
|
||||
fastify.get<{ Params: { user: string; } }>('/users/:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||
if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return;
|
||||
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
const userId = request.params.user;
|
||||
|
||||
|
@ -809,6 +811,8 @@ export class ActivityPubServerService {
|
|||
|
||||
fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||
if (await this.shouldRefuseGetRequest(request, reply, request.params.user)) return;
|
||||
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
usernameLower: request.params.user.toLowerCase(),
|
||||
|
|
|
@ -22,10 +22,14 @@ 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 { MastoConverters } from './api/mastodon/converters.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';
|
||||
|
@ -40,11 +44,10 @@ 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 { MastodonApiServerService } from './api/mastodon/MastodonApiServerService.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: [
|
||||
|
@ -81,6 +84,8 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
|
|||
BubbleTimelineChannelService,
|
||||
HashtagChannelService,
|
||||
RoleTimelineChannelService,
|
||||
ReversiChannelService,
|
||||
ReversiGameChannelService,
|
||||
HomeTimelineChannelService,
|
||||
HybridTimelineChannelService,
|
||||
LocalTimelineChannelService,
|
||||
|
|
|
@ -379,6 +379,12 @@ import * as ep___retention from './endpoints/retention.js';
|
|||
import * as ep___sponsors from './endpoints/sponsors.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';
|
||||
|
@ -756,6 +762,12 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.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: [
|
||||
|
@ -1137,6 +1149,12 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl
|
|||
$sponsors,
|
||||
$bubbleGame_register,
|
||||
$bubbleGame_ranking,
|
||||
$reversi_cancelMatch,
|
||||
$reversi_games,
|
||||
$reversi_match,
|
||||
$reversi_invitations,
|
||||
$reversi_showGame,
|
||||
$reversi_surrender,
|
||||
],
|
||||
exports: [
|
||||
$admin_meta,
|
||||
|
@ -1509,6 +1527,12 @@ const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useCl
|
|||
$sponsors,
|
||||
$bubbleGame_register,
|
||||
$bubbleGame_ranking,
|
||||
$reversi_cancelMatch,
|
||||
$reversi_games,
|
||||
$reversi_match,
|
||||
$reversi_invitations,
|
||||
$reversi_showGame,
|
||||
$reversi_surrender,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule {}
|
||||
|
|
|
@ -380,6 +380,12 @@ import * as ep___retention from './endpoints/retention.js';
|
|||
import * as ep___sponsors from './endpoints/sponsors.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],
|
||||
|
@ -755,6 +761,12 @@ const eps = [
|
|||
['sponsors', ep___sponsors],
|
||||
['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 {
|
||||
|
|
|
@ -63,8 +63,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.invalidSeed);
|
||||
}
|
||||
|
||||
// シードが古すぎる(1時間以上前)のも弾く
|
||||
if (seedDate.getTime() < now.getTime() - 1000 * 60 * 60) {
|
||||
// シードが古すぎる(5時間以上前)のも弾く
|
||||
if (seedDate.getTime() < now.getTime() - 1000 * 60 * 60 * 5) {
|
||||
throw new ApiError(meta.errors.invalidSeed);
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,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: 'write: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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
61
packages/backend/src/server/api/endpoints/reversi/games.ts
Normal file
61
packages/backend/src/server/api/endpoints/reversi/games.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Brackets } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { ReversiGamesRepository } from '@/models/_.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: { ref: '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');
|
||||
|
||||
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: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '0b4f0559-b484-4e31-9581-3f73cee89b28',
|
||||
},
|
||||
|
||||
isYourself: {
|
||||
message: 'Target user is yourself.',
|
||||
code: 'TARGET_IS_YOURSELF',
|
||||
id: '96fd7bd6-d2bc-426c-a865-d055dcd2828e',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id', 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: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchGame: {
|
||||
message: 'No such game.',
|
||||
code: 'NO_SUCH_GAME',
|
||||
id: 'ace0b11f-e0a6-4076-a30d-e8284c81b2df',
|
||||
},
|
||||
|
||||
alreadyEnded: {
|
||||
message: 'That game has already ended.',
|
||||
code: 'ALREADY_ENDED',
|
||||
id: '6c2ad4a6-cbf1-4a5b-b187-b772826cfc6d',
|
||||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'Access denied.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '6e04164b-a992-4c93-8489-2123069973e1',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
gameId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['gameId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -20,6 +20,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()
|
||||
|
@ -40,6 +42,8 @@ export class ChannelsService {
|
|||
private serverStatsChannelService: ServerStatsChannelService,
|
||||
private queueStatsChannelService: QueueStatsChannelService,
|
||||
private adminChannelService: AdminChannelService,
|
||||
private reversiChannelService: ReversiChannelService,
|
||||
private reversiGameChannelService: ReversiGameChannelService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -61,6 +65,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}`);
|
||||
|
|
127
packages/backend/src/server/api/stream/channels/reversi-game.ts
Normal file
127
packages/backend/src/server/api/stream/channels/reversi-game.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 'checkState': this.checkState(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 checkState(crc32: string | number) {
|
||||
if (crc32 != null) return;
|
||||
|
||||
const game = await this.reversiService.checkCrc(this.gameId!, crc32);
|
||||
if (game) {
|
||||
this.send('rescue', 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -494,6 +494,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();
|
||||
|
@ -533,6 +535,8 @@ export class ClientServerService {
|
|||
return;
|
||||
}
|
||||
|
||||
vary(reply.raw, 'Accept');
|
||||
|
||||
reply.redirect(`/@${user.username}${ user.host == null ? '' : '@' + user.host}`);
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue