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

This commit is contained in:
まっちゃとーにゅ
2024-01-24 18:51:48 +09:00
26 changed files with 720 additions and 443 deletions

View File

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

View File

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

View File

@@ -113,6 +113,11 @@ export class MiReversiGame {
})
public bw: string;
@Column('boolean', {
default: false,
})
public noIrregularRules: boolean;
@Column('boolean', {
default: false,
})

View File

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

View File

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

View File

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

View File

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