Skip to content

Commit ab15af3

Browse files
committed
Merge branch 'develop' into prohibit-posting-kyomu-note
2 parents 5dd8c68 + b920435 commit ab15af3

39 files changed

+931
-333
lines changed

locales/index.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,8 @@ export interface Locale {
11991199
"showReplay": string;
12001200
"replay": string;
12011201
"replaying": string;
1202+
"ranking": string;
1203+
"lastNDays": string;
12021204
"_bubbleGame": {
12031205
"howToPlay": string;
12041206
"_howToPlay": {

locales/ja-JP.yml

+2
Original file line numberDiff line numberDiff line change
@@ -1196,6 +1196,8 @@ soundWillBePlayed: "サウンドが再生されます"
11961196
showReplay: "リプレイを見る"
11971197
replay: "リプレイ"
11981198
replaying: "リプレイ中"
1199+
ranking: "ランキング"
1200+
lastNDays: "直近{n}日"
11991201

12001202
_bubbleGame:
12011203
howToPlay: "遊び方"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* SPDX-FileCopyrightText: syuilo and other misskey contributors
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
export class BubbleGameRecord1704959805077 {
7+
name = 'BubbleGameRecord1704959805077'
8+
9+
async up(queryRunner) {
10+
await queryRunner.query(`CREATE TABLE "bubble_game_record" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "seededAt" TIMESTAMP WITH TIME ZONE NOT NULL, "seed" character varying(1024) NOT NULL, "gameVersion" integer NOT NULL, "gameMode" character varying(128) NOT NULL, "score" integer NOT NULL, "logs" jsonb NOT NULL DEFAULT '[]', "isVerified" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_a75395fe404b392e2893b50d7ea" PRIMARY KEY ("id"))`);
11+
await queryRunner.query(`CREATE INDEX "IDX_75276757070d21fdfaf4c05290" ON "bubble_game_record" ("userId") `);
12+
await queryRunner.query(`CREATE INDEX "IDX_4ae7053179014915d1432d3f40" ON "bubble_game_record" ("seededAt") `);
13+
await queryRunner.query(`CREATE INDEX "IDX_26d4ee490b5a487142d35466ee" ON "bubble_game_record" ("score") `);
14+
await queryRunner.query(`ALTER TABLE "bubble_game_record" ADD CONSTRAINT "FK_75276757070d21fdfaf4c052909" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
15+
}
16+
17+
async down(queryRunner) {
18+
await queryRunner.query(`ALTER TABLE "bubble_game_record" DROP CONSTRAINT "FK_75276757070d21fdfaf4c052909"`);
19+
await queryRunner.query(`DROP INDEX "public"."IDX_26d4ee490b5a487142d35466ee"`);
20+
await queryRunner.query(`DROP INDEX "public"."IDX_4ae7053179014915d1432d3f40"`);
21+
await queryRunner.query(`DROP INDEX "public"."IDX_75276757070d21fdfaf4c05290"`);
22+
await queryRunner.query(`DROP TABLE "bubble_game_record"`);
23+
}
24+
}

packages/backend/src/di-symbols.ts

+1
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,6 @@ export const DI = {
7878
flashsRepository: Symbol('flashsRepository'),
7979
flashLikesRepository: Symbol('flashLikesRepository'),
8080
userMemosRepository: Symbol('userMemosRepository'),
81+
bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'),
8182
//#endregion
8283
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* SPDX-FileCopyrightText: syuilo and other misskey contributors
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
7+
import { id } from './util/id.js';
8+
import { MiUser } from './User.js';
9+
10+
@Entity('bubble_game_record')
11+
export class MiBubbleGameRecord {
12+
@PrimaryColumn(id())
13+
public id: string;
14+
15+
@Index()
16+
@Column({
17+
...id(),
18+
})
19+
public userId: MiUser['id'];
20+
21+
@ManyToOne(type => MiUser, {
22+
onDelete: 'CASCADE',
23+
})
24+
@JoinColumn()
25+
public user: MiUser | null;
26+
27+
@Index()
28+
@Column('timestamp with time zone')
29+
public seededAt: Date;
30+
31+
@Column('varchar', {
32+
length: 1024,
33+
})
34+
public seed: string;
35+
36+
@Column('integer')
37+
public gameVersion: number;
38+
39+
@Column('varchar', {
40+
length: 128,
41+
})
42+
public gameMode: string;
43+
44+
@Index()
45+
@Column('integer')
46+
public score: number;
47+
48+
@Column('jsonb', {
49+
default: [],
50+
})
51+
public logs: any[];
52+
53+
@Column('boolean', {
54+
default: false,
55+
})
56+
public isVerified: boolean;
57+
}

packages/backend/src/models/RepositoryModule.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { Module } from '@nestjs/common';
77
import { DI } from '@/di-symbols.js';
8-
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 } from './_.js';
8+
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, MiBubbleGameRecord } from './_.js';
99
import type { DataSource } from 'typeorm';
1010
import type { Provider } from '@nestjs/common';
1111

@@ -399,6 +399,12 @@ const $userMemosRepository: Provider = {
399399
inject: [DI.db],
400400
};
401401

402+
export const $bubbleGameRecordsRepository: Provider = {
403+
provide: DI.bubbleGameRecordsRepository,
404+
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord),
405+
inject: [DI.db],
406+
};
407+
402408
@Module({
403409
imports: [
404410
],
@@ -468,6 +474,7 @@ const $userMemosRepository: Provider = {
468474
$flashsRepository,
469475
$flashLikesRepository,
470476
$userMemosRepository,
477+
$bubbleGameRecordsRepository,
471478
],
472479
exports: [
473480
$usersRepository,
@@ -535,6 +542,7 @@ const $userMemosRepository: Provider = {
535542
$flashsRepository,
536543
$flashLikesRepository,
537544
$userMemosRepository,
545+
$bubbleGameRecordsRepository,
538546
],
539547
})
540548
export class RepositoryModule {}

packages/backend/src/models/_.ts

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
6868
import { MiFlash } from '@/models/Flash.js';
6969
import { MiFlashLike } from '@/models/FlashLike.js';
7070
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
71+
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
7172
import type { Repository } from 'typeorm';
7273

7374
export {
@@ -136,6 +137,7 @@ export {
136137
MiFlash,
137138
MiFlashLike,
138139
MiUserMemo,
140+
MiBubbleGameRecord,
139141
};
140142

141143
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>;
@@ -203,3 +205,4 @@ export type RoleAssignmentsRepository = Repository<MiRoleAssignment>;
203205
export type FlashsRepository = Repository<MiFlash>;
204206
export type FlashLikesRepository = Repository<MiFlashLike>;
205207
export type UserMemoRepository = Repository<MiUserMemo>;
208+
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>;

packages/backend/src/postgres.ts

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import { MiRoleAssignment } from '@/models/RoleAssignment.js';
7676
import { MiFlash } from '@/models/Flash.js';
7777
import { MiFlashLike } from '@/models/FlashLike.js';
7878
import { MiUserMemo } from '@/models/UserMemo.js';
79+
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
7980

8081
import { Config } from '@/config.js';
8182
import MisskeyLogger from '@/logger.js';
@@ -190,6 +191,7 @@ export const entities = [
190191
MiFlash,
191192
MiFlashLike,
192193
MiUserMemo,
194+
MiBubbleGameRecord,
193195
...charts,
194196
];
195197

packages/backend/src/server/api/EndpointsModule.ts

+8
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,8 @@ import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
364364
import * as ep___fetchRss from './endpoints/fetch-rss.js';
365365
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
366366
import * as ep___retention from './endpoints/retention.js';
367+
import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
368+
import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
367369
import { GetterService } from './GetterService.js';
368370
import { ApiLoggerService } from './ApiLoggerService.js';
369371
import type { Provider } from '@nestjs/common';
@@ -726,6 +728,8 @@ const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass:
726728
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
727729
const $fetchExternalResources: Provider = { provide: 'ep:fetch-external-resources', useClass: ep___fetchExternalResources.default };
728730
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
731+
const $bubbleGame_register: Provider = { provide: 'ep:bubble-game/register', useClass: ep___bubbleGame_register.default };
732+
const $bubbleGame_ranking: Provider = { provide: 'ep:bubble-game/ranking', useClass: ep___bubbleGame_ranking.default };
729733

730734
@Module({
731735
imports: [
@@ -1092,6 +1096,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
10921096
$fetchRss,
10931097
$fetchExternalResources,
10941098
$retention,
1099+
$bubbleGame_register,
1100+
$bubbleGame_ranking,
10951101
],
10961102
exports: [
10971103
$admin_meta,
@@ -1449,6 +1455,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
14491455
$fetchRss,
14501456
$fetchExternalResources,
14511457
$retention,
1458+
$bubbleGame_register,
1459+
$bubbleGame_ranking,
14521460
],
14531461
})
14541462
export class EndpointsModule {}

packages/backend/src/server/api/endpoints.ts

+4
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
365365
import * as ep___fetchRss from './endpoints/fetch-rss.js';
366366
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
367367
import * as ep___retention from './endpoints/retention.js';
368+
import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
369+
import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
368370

369371
const eps = [
370372
['admin/meta', ep___admin_meta],
@@ -725,6 +727,8 @@ const eps = [
725727
['fetch-rss', ep___fetchRss],
726728
['fetch-external-resources', ep___fetchExternalResources],
727729
['retention', ep___retention],
730+
['bubble-game/register', ep___bubbleGame_register],
731+
['bubble-game/ranking', ep___bubbleGame_ranking],
728732
];
729733

730734
interface IEndpointMetaBase {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* SPDX-FileCopyrightText: syuilo and other misskey contributors
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
import { Inject, Injectable } from '@nestjs/common';
7+
import { MoreThan } from 'typeorm';
8+
import { Endpoint } from '@/server/api/endpoint-base.js';
9+
import type { BubbleGameRecordsRepository } from '@/models/_.js';
10+
import { DI } from '@/di-symbols.js';
11+
import { UserEntityService } from '@/core/entities/UserEntityService.js';
12+
13+
export const meta = {
14+
allowGet: true,
15+
cacheSec: 60,
16+
17+
errors: {
18+
},
19+
20+
res: {
21+
type: 'array',
22+
optional: false, nullable: false,
23+
items: {
24+
type: 'object',
25+
optional: false, nullable: false,
26+
properties: {
27+
id: { type: 'string', format: 'misskey:id' },
28+
score: { type: 'integer' },
29+
user: { ref: 'UserLite' },
30+
},
31+
},
32+
},
33+
} as const;
34+
35+
export const paramDef = {
36+
type: 'object',
37+
properties: {
38+
gameMode: { type: 'string' },
39+
},
40+
required: ['gameMode'],
41+
} as const;
42+
43+
@Injectable()
44+
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
45+
constructor(
46+
@Inject(DI.bubbleGameRecordsRepository)
47+
private bubbleGameRecordsRepository: BubbleGameRecordsRepository,
48+
49+
private userEntityService: UserEntityService,
50+
) {
51+
super(meta, paramDef, async (ps) => {
52+
const records = await this.bubbleGameRecordsRepository.find({
53+
where: {
54+
gameMode: ps.gameMode,
55+
seededAt: MoreThan(new Date(Date.now() - 1000 * 60 * 60 * 24 * 7)),
56+
},
57+
order: {
58+
score: 'DESC',
59+
},
60+
take: 10,
61+
relations: ['user'],
62+
});
63+
64+
const users = await this.userEntityService.packMany(records.map(r => r.user!), null, { detail: false });
65+
66+
return records.map(r => ({
67+
id: r.id,
68+
score: r.score,
69+
user: users.find(u => u.id === r.user!.id),
70+
}));
71+
});
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* SPDX-FileCopyrightText: syuilo and other misskey contributors
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
import { Inject, Injectable } from '@nestjs/common';
7+
import ms from 'ms';
8+
import { Endpoint } from '@/server/api/endpoint-base.js';
9+
import { IdService } from '@/core/IdService.js';
10+
import type { BubbleGameRecordsRepository } from '@/models/_.js';
11+
import { DI } from '@/di-symbols.js';
12+
import { ApiError } from '../../error.js';
13+
14+
export const meta = {
15+
requireCredential: true,
16+
17+
kind: 'write:account',
18+
19+
limit: {
20+
duration: ms('1hour'),
21+
max: 120,
22+
minInterval: ms('30sec'),
23+
},
24+
25+
errors: {
26+
invalidSeed: {
27+
message: 'Provided seed is invalid.',
28+
code: 'INVALID_SEED',
29+
id: 'eb627bc7-574b-4a52-a860-3c3eae772b88',
30+
},
31+
},
32+
33+
res: {
34+
},
35+
} as const;
36+
37+
export const paramDef = {
38+
type: 'object',
39+
properties: {
40+
score: { type: 'integer', minimum: 0 },
41+
seed: { type: 'string', minLength: 1, maxLength: 1024 },
42+
logs: { type: 'array' },
43+
gameMode: { type: 'string' },
44+
gameVersion: { type: 'integer' },
45+
},
46+
required: ['score', 'seed', 'logs', 'gameMode', 'gameVersion'],
47+
} as const;
48+
49+
@Injectable()
50+
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
51+
constructor(
52+
@Inject(DI.bubbleGameRecordsRepository)
53+
private bubbleGameRecordsRepository: BubbleGameRecordsRepository,
54+
55+
private idService: IdService,
56+
) {
57+
super(meta, paramDef, async (ps, me) => {
58+
const seedDate = new Date(parseInt(ps.seed, 10));
59+
const now = new Date();
60+
61+
// シードが未来なのは通常のプレイではありえないので弾く
62+
if (seedDate.getTime() > now.getTime()) {
63+
throw new ApiError(meta.errors.invalidSeed);
64+
}
65+
66+
// シードが古すぎる(1時間以上前)のも弾く
67+
if (seedDate.getTime() < now.getTime() - 1000 * 60 * 60) {
68+
throw new ApiError(meta.errors.invalidSeed);
69+
}
70+
71+
await this.bubbleGameRecordsRepository.insert({
72+
id: this.idService.gen(now.getTime()),
73+
seed: ps.seed,
74+
seededAt: seedDate,
75+
userId: me.id,
76+
score: ps.score,
77+
logs: ps.logs,
78+
gameMode: ps.gameMode,
79+
gameVersion: ps.gameVersion,
80+
isVerified: false,
81+
});
82+
});
83+
}
84+
}
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)