Merge remote-tracking branch 'misskey-original/develop' into develop
# Conflicts: # packages/backend/src/models/RepositoryModule.ts # packages/backend/src/models/_.ts # packages/backend/src/postgres.ts # packages/frontend/src/components/MkDrive.file.vue # packages/frontend/src/components/MkEmojiPicker.vue # packages/frontend/src/pages/admin/index.vue # packages/frontend/src/pages/user/home.vue # packages/frontend/src/router.ts # packages/frontend/src/ui/universal.vue
This commit is contained in:
commit
33507e24ff
157 changed files with 3973 additions and 2059 deletions
|
|
@ -3,7 +3,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Global, Inject, Module } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
|
@ -12,6 +11,7 @@ import { DI } from './di-symbols.js';
|
|||
import { Config, loadConfig } from './config.js';
|
||||
import { createPostgresDataSource } from './postgres.js';
|
||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||
import { allSettled } from './misc/promise-tracker.js';
|
||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
const $config: Provider = {
|
||||
|
|
@ -33,7 +33,7 @@ const $meilisearch: Provider = {
|
|||
useFactory: (config: Config) => {
|
||||
if (config.meilisearch) {
|
||||
return new MeiliSearch({
|
||||
host: `${config.meilisearch.ssl ? 'https' : 'http' }://${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||
host: `${config.meilisearch.ssl ? 'https' : 'http'}://${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||
apiKey: config.meilisearch.apiKey,
|
||||
});
|
||||
} else {
|
||||
|
|
@ -91,17 +91,12 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
|
||||
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
|
||||
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// XXX:
|
||||
// Shutting down the existing connections causes errors on Jest as
|
||||
// Misskey has asynchronous postgres/redis connections that are not
|
||||
// awaited.
|
||||
// Let's wait for some random time for them to finish.
|
||||
await setTimeout(5000);
|
||||
}
|
||||
// Wait for all potential DB queries
|
||||
await allSettled();
|
||||
// And then disconnect from DB
|
||||
await Promise.all([
|
||||
this.db.destroy(),
|
||||
this.redisClient.disconnect(),
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ export const ACHIEVEMENT_TYPES = [
|
|||
'brainDiver',
|
||||
'smashTestNotificationButton',
|
||||
'tutorialCompleted',
|
||||
'bubbleGameExplodingHead',
|
||||
'bubbleGameDoubleExplodingHead',
|
||||
] as const;
|
||||
|
||||
@Injectable()
|
||||
|
|
|
|||
|
|
@ -655,7 +655,7 @@ export class DriveService {
|
|||
public async updateFile(file: MiDriveFile, values: Partial<MiDriveFile>, updater: MiUser) {
|
||||
const alwaysMarkNsfw = (await this.roleService.getUserPolicies(file.userId)).alwaysMarkNsfw;
|
||||
|
||||
if (values.name && !this.driveFileEntityService.validateFileName(file.name)) {
|
||||
if (values.name != null && !this.driveFileEntityService.validateFileName(values.name)) {
|
||||
throw new DriveService.InvalidFileNameError();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
|||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
|
@ -644,7 +645,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
this.relayService.deliverToRelays(user, noteActivity);
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
})();
|
||||
}
|
||||
//#endregion
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { IdService } from '@/core/IdService.js';
|
|||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteReadService implements OnApplicationShutdown {
|
||||
|
|
@ -107,7 +108,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
|
||||
// TODO: ↓まとめてクエリしたい
|
||||
|
||||
this.noteUnreadsRepository.countBy({
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isMentioned: true,
|
||||
}).then(mentionsCount => {
|
||||
|
|
@ -115,9 +116,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.noteUnreadsRepository.countBy({
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isSpecified: true,
|
||||
}).then(specifiedCount => {
|
||||
|
|
@ -125,7 +126,7 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { CacheService } from '@/core/CacheService.js';
|
|||
import type { Config } from '@/config.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
import type { FilterUnionByProperty } from '@/types.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationService implements OnApplicationShutdown {
|
||||
|
|
@ -74,7 +75,18 @@ export class NotificationService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async createNotification<T extends MiNotification['type']>(
|
||||
public createNotification<T extends MiNotification['type']>(
|
||||
notifieeId: MiUser['id'],
|
||||
type: T,
|
||||
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||
notifierId?: MiUser['id'] | null,
|
||||
) {
|
||||
trackPromise(
|
||||
this.#createNotificationInternal(notifieeId, type, data, notifierId),
|
||||
);
|
||||
}
|
||||
|
||||
async #createNotificationInternal<T extends MiNotification['type']>(
|
||||
notifieeId: MiUser['id'],
|
||||
type: T,
|
||||
data: Omit<FilterUnionByProperty<MiNotification, 'type', T>, 'type' | 'id' | 'createdAt' | 'notifierId'>,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { Inject, Module, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as Bull from 'bullmq';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { QUEUE, baseQueueOptions } from '@/queue/const.js';
|
||||
import { allSettled } from '@/misc/promise-tracker.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, WebhookDeliverJobData, RelationshipJobData, ScheduleNotePostJobData } from '../queue/types.js';
|
||||
|
||||
|
|
@ -116,14 +116,9 @@ export class QueueModule implements OnApplicationShutdown {
|
|||
) {}
|
||||
|
||||
public async dispose(): Promise<void> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
// XXX:
|
||||
// Shutting down the existing connections causes errors on Jest as
|
||||
// Misskey has asynchronous postgres/redis connections that are not
|
||||
// awaited.
|
||||
// Let's wait for some random time for them to finish.
|
||||
await setTimeout(5000);
|
||||
}
|
||||
// Wait for all potential queue jobs
|
||||
await allSettled();
|
||||
// And then close all queues
|
||||
await Promise.all([
|
||||
this.systemQueue.close(),
|
||||
this.endedPollNotificationQueue.close(),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { UserBlockingService } from '@/core/UserBlockingService.js';
|
|||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
|
||||
const FALLBACK = '❤';
|
||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
||||
|
|
@ -268,7 +269,7 @@ export class ReactionService {
|
|||
}
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
|
@ -316,7 +317,7 @@ export class ReactionService {
|
|||
dm.addDirectRecipe(reactee as MiRemoteUser);
|
||||
}
|
||||
dm.addFollowersRecipe();
|
||||
dm.execute();
|
||||
trackPromise(dm.execute());
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class DeliverManager {
|
|||
}
|
||||
|
||||
// deliver
|
||||
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||
await this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,5 +80,6 @@ export const DI = {
|
|||
flashsRepository: Symbol('flashsRepository'),
|
||||
flashLikesRepository: Symbol('flashLikesRepository'),
|
||||
userMemosRepository: Symbol('userMemosRepository'),
|
||||
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
|
||||
//#endregion
|
||||
};
|
||||
|
|
|
|||
23
packages/backend/src/misc/promise-tracker.ts
Normal file
23
packages/backend/src/misc/promise-tracker.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const promiseRefs: Set<WeakRef<Promise<unknown>>> = new Set();
|
||||
|
||||
/**
|
||||
* This tracks promises that other modules decided not to wait for,
|
||||
* and makes sure they are all settled before fully closing down the server.
|
||||
*/
|
||||
export function trackPromise(promise: Promise<unknown>) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
return;
|
||||
}
|
||||
const ref = new WeakRef(promise);
|
||||
promiseRefs.add(ref);
|
||||
promise.finally(() => promiseRefs.delete(ref));
|
||||
}
|
||||
|
||||
export async function allSettled(): Promise<void> {
|
||||
await Promise.allSettled([...promiseRefs].map(r => r.deref()));
|
||||
}
|
||||
57
packages/backend/src/models/BubbleGameRecord.ts
Normal file
57
packages/backend/src/models/BubbleGameRecord.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
@Entity('bubble_game_record')
|
||||
export class MiBubbleGameRecord {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
})
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone')
|
||||
public seededAt: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public seed: string;
|
||||
|
||||
@Column('integer')
|
||||
public gameVersion: number;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
})
|
||||
public gameMode: string;
|
||||
|
||||
@Index()
|
||||
@Column('integer')
|
||||
public score: number;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public logs: any[];
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isVerified: boolean;
|
||||
}
|
||||
|
|
@ -72,6 +72,7 @@ import {
|
|||
MiUserSecurityKey,
|
||||
MiWebhook,
|
||||
MiScheduledNote,
|
||||
MiBubbleGameRecord
|
||||
} from './_.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
|
@ -478,6 +479,12 @@ const $userMemosRepository: Provider = {
|
|||
inject: [DI.db],
|
||||
};
|
||||
|
||||
export const $bubbleGameRecordsRepository: Provider = {
|
||||
provide: DI.bubbleGameRecordsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
],
|
||||
|
|
@ -549,6 +556,7 @@ const $userMemosRepository: Provider = {
|
|||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
],
|
||||
exports: [
|
||||
$usersRepository,
|
||||
|
|
@ -618,6 +626,7 @@ const $userMemosRepository: Provider = {
|
|||
$flashsRepository,
|
||||
$flashLikesRepository,
|
||||
$userMemosRepository,
|
||||
$bubbleGameRecordsRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
|
|||
import { MiFlash } from '@/models/Flash.js';
|
||||
import { MiFlashLike } from '@/models/FlashLike.js';
|
||||
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiScheduledNote } from './ScheduledNote.js';
|
||||
import type { Repository } from 'typeorm';
|
||||
|
||||
|
|
@ -140,6 +141,7 @@ export {
|
|||
MiFlash,
|
||||
MiFlashLike,
|
||||
MiUserMemo,
|
||||
MiBubbleGameRecord,
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>;
|
||||
|
|
@ -209,3 +211,4 @@ export type RoleAssignmentsRepository = Repository<MiRoleAssignment>;
|
|||
export type FlashsRepository = Repository<MiFlash>;
|
||||
export type FlashLikesRepository = Repository<MiFlashLike>;
|
||||
export type UserMemoRepository = Repository<MiUserMemo>;
|
||||
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>;
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ import { MiFlash } from '@/models/Flash.js';
|
|||
import { MiFlashLike } from '@/models/FlashLike.js';
|
||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||
import { MiScheduledNote } from '@/models/ScheduledNote.js';
|
||||
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
|
||||
import { Config } from '@/config.js';
|
||||
import MisskeyLogger from '@/logger.js';
|
||||
|
|
@ -194,6 +195,7 @@ export const entities = [
|
|||
MiFlash,
|
||||
MiFlashLike,
|
||||
MiUserMemo,
|
||||
MiBubbleGameRecord,
|
||||
...charts,
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -374,6 +374,8 @@ import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
|||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
|
||||
import * as ep___retention from './endpoints/retention.js';
|
||||
import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
|
||||
import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
|
||||
import { GetterService } from './GetterService.js';
|
||||
import { ApiLoggerService } from './ApiLoggerService.js';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
|
@ -746,6 +748,8 @@ const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass:
|
|||
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
|
||||
const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default };
|
||||
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
|
||||
const $bubbleGame_register: Provider = { provide: 'ep:bubble-game/register', useClass: ep___bubbleGame_register.default };
|
||||
const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useClass: ep___bubbleGame_ranking.default };
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -1122,6 +1126,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$fetchRss,
|
||||
$fetchExternalResources,
|
||||
$retention,
|
||||
$bubbleGame_register,
|
||||
$bubbleGame_ranking,
|
||||
],
|
||||
exports: [
|
||||
$admin_meta,
|
||||
|
|
@ -1489,6 +1495,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$fetchRss,
|
||||
$fetchExternalResources,
|
||||
$retention,
|
||||
$bubbleGame_register,
|
||||
$bubbleGame_ranking,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule {}
|
||||
|
|
|
|||
|
|
@ -374,6 +374,8 @@ import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
|||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
|
||||
import * as ep___retention from './endpoints/retention.js';
|
||||
import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
|
||||
import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
|
||||
|
||||
const eps = [
|
||||
['admin/meta', ep___admin_meta],
|
||||
|
|
@ -744,6 +746,8 @@ const eps = [
|
|||
['fetch-rss', ep___fetchRss],
|
||||
['fetch-external-resources', ep___fetchExternalResources],
|
||||
['retention', ep___retention],
|
||||
['bubble-game/register', ep___bubbleGame_register],
|
||||
['bubble-game/ranking', ep___bubbleGame_ranking],
|
||||
];
|
||||
|
||||
interface IEndpointMetaBase {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|||
import { IdService } from '@/core/IdService.js';
|
||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
|
@ -92,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
antenna.isActive = true;
|
||||
antenna.lastUsedAt = new Date();
|
||||
this.antennasRepository.update(antenna.id, antenna);
|
||||
trackPromise(this.antennasRepository.update(antenna.id, antenna));
|
||||
|
||||
if (needPublishEvent) {
|
||||
this.globalEventService.publishInternalEvent('antennaUpdated', antenna);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { BubbleGameRecordsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: [],
|
||||
|
||||
allowGet: true,
|
||||
cacheSec: 60,
|
||||
|
||||
errors: {
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
score: { type: 'integer' },
|
||||
user: { ref: 'UserLite' },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
gameMode: { type: 'string' },
|
||||
},
|
||||
required: ['gameMode'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.bubbleGameRecordsRepository)
|
||||
private bubbleGameRecordsRepository: BubbleGameRecordsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
const records = await this.bubbleGameRecordsRepository.find({
|
||||
where: {
|
||||
gameMode: ps.gameMode,
|
||||
seededAt: MoreThan(new Date(Date.now() - 1000 * 60 * 60 * 24 * 7)),
|
||||
},
|
||||
order: {
|
||||
score: 'DESC',
|
||||
},
|
||||
take: 10,
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
const users = await this.userEntityService.packMany(records.map(r => r.user!), null, { detail: false });
|
||||
|
||||
return records.map(r => ({
|
||||
id: r.id,
|
||||
score: r.score,
|
||||
user: users.find(u => u.id === r.user!.id),
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { BubbleGameRecordsRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: [],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 120,
|
||||
minInterval: ms('30sec'),
|
||||
},
|
||||
|
||||
errors: {
|
||||
invalidSeed: {
|
||||
message: 'Provided seed is invalid.',
|
||||
code: 'INVALID_SEED',
|
||||
id: 'eb627bc7-574b-4a52-a860-3c3eae772b88',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
score: { type: 'integer', minimum: 0 },
|
||||
seed: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
logs: { type: 'array' },
|
||||
gameMode: { type: 'string' },
|
||||
gameVersion: { type: 'integer' },
|
||||
},
|
||||
required: ['score', 'seed', 'logs', 'gameMode', 'gameVersion'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.bubbleGameRecordsRepository)
|
||||
private bubbleGameRecordsRepository: BubbleGameRecordsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const seedDate = new Date(parseInt(ps.seed, 10));
|
||||
const now = new Date();
|
||||
|
||||
// シードが未来なのは通常のプレイではありえないので弾く
|
||||
if (seedDate.getTime() > now.getTime()) {
|
||||
throw new ApiError(meta.errors.invalidSeed);
|
||||
}
|
||||
|
||||
// シードが古すぎる(1時間以上前)のも弾く
|
||||
if (seedDate.getTime() < now.getTime() - 1000 * 60 * 60) {
|
||||
throw new ApiError(meta.errors.invalidSeed);
|
||||
}
|
||||
|
||||
await this.bubbleGameRecordsRepository.insert({
|
||||
id: this.idService.gen(now.getTime()),
|
||||
seed: ps.seed,
|
||||
seededAt: seedDate,
|
||||
userId: me.id,
|
||||
score: ps.score,
|
||||
logs: ps.logs,
|
||||
gameMode: ps.gameMode,
|
||||
gameVersion: ps.gameVersion,
|
||||
isVerified: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue