Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
16
packages/backend/migration/1706081514499-reversi-6.js
Normal file
16
packages/backend/migration/1706081514499-reversi-6.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class Reversi61706081514499 {
|
||||
name = 'Reversi61706081514499'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" ADD "noIrregularRules" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "reversi_game" DROP COLUMN "noIrregularRules"`);
|
||||
}
|
||||
}
|
@@ -81,13 +81,13 @@
|
||||
"@fastify/view": "8.2.0",
|
||||
"@misskey-dev/sharp-read-bmp": "^1.1.1",
|
||||
"@misskey-dev/summaly": "^5.0.3",
|
||||
"@nestjs/common": "10.2.10",
|
||||
"@nestjs/core": "10.2.10",
|
||||
"@nestjs/testing": "10.2.10",
|
||||
"@nestjs/common": "10.3.1",
|
||||
"@nestjs/core": "10.3.1",
|
||||
"@nestjs/testing": "10.3.1",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@simplewebauthn/server": "9.0.0",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"@smithy/node-http-handler": "2.1.10",
|
||||
"@smithy/node-http-handler": "2.3.1",
|
||||
"@swc/cli": "0.1.65",
|
||||
"@swc/core": "1.3.105",
|
||||
"@twemoji/parser": "15.0.0",
|
||||
@@ -98,7 +98,7 @@
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.2",
|
||||
"bullmq": "5.1.4",
|
||||
"bullmq": "5.1.5",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.1",
|
||||
"chalk": "5.3.0",
|
||||
@@ -186,7 +186,7 @@
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||
"@nestjs/platform-express": "10.3.0",
|
||||
"@nestjs/platform-express": "10.3.1",
|
||||
"@simplewebauthn/types": "9.0.0",
|
||||
"@swc/jest": "0.2.31",
|
||||
"@types/accepts": "1.3.7",
|
||||
@@ -204,7 +204,7 @@
|
||||
"@types/jsrsasign": "10.5.12",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "20.11.5",
|
||||
"@types/node": "20.11.6",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/oauth": "0.9.4",
|
||||
"@types/oauth2orize": "1.11.3",
|
||||
|
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import * as Reversi from 'misskey-reversi';
|
||||
import { LessThan, MoreThan } from 'typeorm';
|
||||
import type {
|
||||
MiReversiGame,
|
||||
ReversiGamesRepository,
|
||||
@@ -23,7 +24,7 @@ import { Serialized } from '@/types.js';
|
||||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
|
||||
const MATCHING_TIMEOUT_MS = 1000 * 15; // 15sec
|
||||
const INVITATION_TIMEOUT_MS = 1000 * 20; // 20sec
|
||||
|
||||
@Injectable()
|
||||
export class ReversiService implements OnModuleInit {
|
||||
@@ -84,44 +85,82 @@ export class ReversiService implements OnModuleInit {
|
||||
map: game.map,
|
||||
bw: game.bw,
|
||||
crc32: game.crc32,
|
||||
noIrregularRules: game.noIrregularRules,
|
||||
} satisfies Partial<MiReversiGame>;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async matchSpecificUser(me: MiUser, targetUser: MiUser): Promise<MiReversiGame | null> {
|
||||
public async matchSpecificUser(me: MiUser, targetUser: MiUser, multiple = false): Promise<MiReversiGame | null> {
|
||||
if (targetUser.id === me.id) {
|
||||
throw new Error('You cannot match yourself.');
|
||||
}
|
||||
|
||||
if (!multiple) {
|
||||
// 既にマッチしている対局が無いか探す(3分以内)
|
||||
const games = await this.reversiGamesRepository.find({
|
||||
where: [
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, user2Id: targetUser.id, isStarted: false },
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: targetUser.id, user2Id: me.id, isStarted: false },
|
||||
],
|
||||
relations: ['user1', 'user2'],
|
||||
order: { id: 'DESC' },
|
||||
});
|
||||
if (games.length > 0) {
|
||||
return games[0];
|
||||
}
|
||||
}
|
||||
|
||||
//#region 相手から既に招待されてないか確認
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${me.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
Date.now() - INVITATION_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
|
||||
if (invitations.includes(targetUser.id)) {
|
||||
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, targetUser.id);
|
||||
|
||||
const game = await this.matched(targetUser.id, me.id);
|
||||
|
||||
return game;
|
||||
} else {
|
||||
this.redisClient.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
|
||||
|
||||
this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
|
||||
user: await this.userEntityService.pack(me, targetUser),
|
||||
const game = await this.matched(targetUser.id, me.id, {
|
||||
noIrregularRules: false,
|
||||
});
|
||||
|
||||
return null;
|
||||
return game;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.zadd(`reversi:matchSpecific:${targetUser.id}`, Date.now(), me.id);
|
||||
redisPipeline.expire(`reversi:matchSpecific:${targetUser.id}`, 120, 'NX');
|
||||
await redisPipeline.exec();
|
||||
|
||||
this.globalEventService.publishReversiStream(targetUser.id, 'invited', {
|
||||
user: await this.userEntityService.pack(me, targetUser),
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async matchAnyUser(me: MiUser): Promise<MiReversiGame | null> {
|
||||
public async matchAnyUser(me: MiUser, options: { noIrregularRules: boolean }, multiple = false): Promise<MiReversiGame | null> {
|
||||
if (!multiple) {
|
||||
// 既にマッチしている対局が無いか探す(3分以内)
|
||||
const games = await this.reversiGamesRepository.find({
|
||||
where: [
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user1Id: me.id, isStarted: false },
|
||||
{ id: MoreThan(this.idService.gen(Date.now() - 1000 * 60 * 3)), user2Id: me.id, isStarted: false },
|
||||
],
|
||||
relations: ['user1', 'user2'],
|
||||
order: { id: 'DESC' },
|
||||
});
|
||||
if (games.length > 0) {
|
||||
return games[0];
|
||||
}
|
||||
}
|
||||
|
||||
//#region まず自分宛ての招待を探す
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${me.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
Date.now() - INVITATION_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
|
||||
@@ -129,7 +168,9 @@ export class ReversiService implements OnModuleInit {
|
||||
const invitorId = invitations[Math.floor(Math.random() * invitations.length)];
|
||||
await this.redisClient.zrem(`reversi:matchSpecific:${me.id}`, invitorId);
|
||||
|
||||
const game = await this.matched(invitorId, me.id);
|
||||
const game = await this.matched(invitorId, me.id, {
|
||||
noIrregularRules: false,
|
||||
});
|
||||
|
||||
return game;
|
||||
}
|
||||
@@ -137,23 +178,35 @@ export class ReversiService implements OnModuleInit {
|
||||
|
||||
const matchings = await this.redisClient.zrange(
|
||||
'reversi:matchAny',
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
0,
|
||||
2, // 自分自身のIDが入っている場合もあるので2つ取得
|
||||
'REV');
|
||||
|
||||
const userIds = matchings.filter(id => id !== me.id);
|
||||
const items = matchings.filter(id => !id.startsWith(me.id));
|
||||
|
||||
if (userIds.length > 0) {
|
||||
// pick random
|
||||
const matchedUserId = userIds[Math.floor(Math.random() * userIds.length)];
|
||||
if (items.length > 0) {
|
||||
const [matchedUserId, option] = items[0].split(':');
|
||||
|
||||
await this.redisClient.zrem('reversi:matchAny', me.id, matchedUserId);
|
||||
await this.redisClient.zrem('reversi:matchAny',
|
||||
me.id,
|
||||
matchedUserId,
|
||||
me.id + ':noIrregularRules',
|
||||
matchedUserId + ':noIrregularRules');
|
||||
|
||||
const game = await this.matched(matchedUserId, me.id);
|
||||
const game = await this.matched(matchedUserId, me.id, {
|
||||
noIrregularRules: options.noIrregularRules || option === 'noIrregularRules',
|
||||
});
|
||||
|
||||
return game;
|
||||
} else {
|
||||
await this.redisClient.zadd('reversi:matchAny', Date.now(), me.id);
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
if (options.noIrregularRules) {
|
||||
redisPipeline.zadd('reversi:matchAny', Date.now(), me.id + ':noIrregularRules');
|
||||
} else {
|
||||
redisPipeline.zadd('reversi:matchAny', Date.now(), me.id);
|
||||
}
|
||||
redisPipeline.expire('reversi:matchAny', 15, 'NX');
|
||||
await redisPipeline.exec();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -165,7 +218,15 @@ export class ReversiService implements OnModuleInit {
|
||||
|
||||
@bindThis
|
||||
public async matchAnyUserCancel(user: MiUser) {
|
||||
await this.redisClient.zrem('reversi:matchAny', user.id);
|
||||
await this.redisClient.zrem('reversi:matchAny', user.id, user.id + ':noIrregularRules');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async cleanOutdatedGames() {
|
||||
await this.reversiGamesRepository.delete({
|
||||
id: LessThan(this.idService.gen(Date.now() - 1000 * 60 * 10)),
|
||||
isStarted: false,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -219,7 +280,7 @@ export class ReversiService implements OnModuleInit {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async matched(parentId: MiUser['id'], childId: MiUser['id']): Promise<MiReversiGame> {
|
||||
private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> {
|
||||
const game = await this.reversiGamesRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
user1Id: parentId,
|
||||
@@ -232,6 +293,7 @@ export class ReversiService implements OnModuleInit {
|
||||
map: Reversi.maps.eighteight.data,
|
||||
bw: 'random',
|
||||
isLlotheo: false,
|
||||
noIrregularRules: options.noIrregularRules,
|
||||
}).then(x => this.reversiGamesRepository.findOneOrFail({
|
||||
where: { id: x.identifiers[0].id },
|
||||
relations: ['user1', 'user2'],
|
||||
@@ -333,7 +395,7 @@ export class ReversiService implements OnModuleInit {
|
||||
public async getInvitations(user: MiUser): Promise<MiUser['id'][]> {
|
||||
const invitations = await this.redisClient.zrange(
|
||||
`reversi:matchSpecific:${user.id}`,
|
||||
Date.now() - MATCHING_TIMEOUT_MS,
|
||||
Date.now() - INVITATION_TIMEOUT_MS,
|
||||
'+inf',
|
||||
'BYSCORE');
|
||||
return invitations;
|
||||
|
@@ -60,6 +60,7 @@ export class ReversiGameEntityService {
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
timeLimitForEachTurn: game.timeLimitForEachTurn,
|
||||
noIrregularRules: game.noIrregularRules,
|
||||
logs: game.logs,
|
||||
map: game.map,
|
||||
});
|
||||
@@ -108,6 +109,7 @@ export class ReversiGameEntityService {
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
timeLimitForEachTurn: game.timeLimitForEachTurn,
|
||||
noIrregularRules: game.noIrregularRules,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -113,6 +113,11 @@ export class MiReversiGame {
|
||||
})
|
||||
public bw: string;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public noIrregularRules: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
@@ -82,6 +82,10 @@ export const packedReversiGameLiteSchema = {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
noIrregularRules: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isLlotheo: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
@@ -196,6 +200,10 @@ export const packedReversiGameDetailedSchema = {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
noIrregularRules: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isLlotheo: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@@ -11,6 +11,7 @@ import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ReversiService } from '@/core/ReversiService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
@@ -32,6 +33,7 @@ export class CleanProcessorService {
|
||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private reversiService: ReversiService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('clean');
|
||||
@@ -65,6 +67,8 @@ export class CleanProcessorService {
|
||||
});
|
||||
}
|
||||
|
||||
this.reversiService.cleanOutdatedGames();
|
||||
|
||||
this.logger.succ('Cleaned.');
|
||||
}
|
||||
}
|
||||
|
@@ -43,7 +43,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.reversiGamesRepository.createQueryBuilder('game'), ps.sinceId, ps.untilId)
|
||||
.andWhere('game.isStarted = TRUE')
|
||||
.innerJoinAndSelect('game.user1', 'user1')
|
||||
.innerJoinAndSelect('game.user2', 'user2');
|
||||
|
||||
@@ -53,6 +52,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
.where('game.user1Id = :userId', { userId: me.id })
|
||||
.orWhere('game.user2Id = :userId', { userId: me.id });
|
||||
}));
|
||||
} else {
|
||||
query.andWhere('game.isStarted = TRUE');
|
||||
}
|
||||
|
||||
const games = await query.take(ps.limit).getMany();
|
||||
|
@@ -37,6 +37,8 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
noIrregularRules: { type: 'boolean', default: false },
|
||||
multiple: { type: 'boolean', default: false },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@@ -56,7 +58,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw err;
|
||||
}) : null;
|
||||
|
||||
const game = target ? await this.reversiService.matchSpecificUser(me, target) : await this.reversiService.matchAnyUser(me);
|
||||
const game = target
|
||||
? await this.reversiService.matchSpecificUser(me, target, ps.multiple)
|
||||
: await this.reversiService.matchAnyUser(me, { noIrregularRules: ps.noIrregularRules }, ps.multiple);
|
||||
|
||||
if (game == null) return;
|
||||
|
||||
|
Reference in New Issue
Block a user