diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index bc6d24b951..0ad91ff308 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -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,9 @@ 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 { ReversiMatchingEntityService } from './entities/ReversiMatchingEntityService.js'; + import { ApAudienceService } from './activitypub/ApAudienceService.js'; import { ApDbResolverService } from './activitypub/ApDbResolverService.js'; import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js'; @@ -199,6 +205,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 +254,8 @@ 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 $ReversiMatchingEntityService: Provider = { provide: 'ReversiMatchingEntityService', useExisting: ReversiMatchingEntityService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; @@ -336,6 +345,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, + ReversiService, + ChartLoggerService, FederationChart, NotesChart, @@ -350,6 +361,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PerUserDriveChart, ApRequestChart, ChartManagementService, + AbuseUserReportEntityService, AntennaEntityService, AppEntityService, @@ -382,6 +394,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FlashEntityService, FlashLikeEntityService, RoleEntityService, + ReversiGameEntityService, + ReversiMatchingEntityService, + ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -466,6 +481,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, + $ReversiService, + $ChartLoggerService, $FederationChart, $NotesChart, @@ -480,6 +497,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PerUserDriveChart, $ApRequestChart, $ChartManagementService, + $AbuseUserReportEntityService, $AntennaEntityService, $AppEntityService, @@ -512,6 +530,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, + $ReversiGameEntityService, + $ReversiMatchingEntityService, + $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, @@ -597,6 +618,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FanoutTimelineEndpointService, ChannelFollowingService, RegistryApiService, + ReversiService, + FederationChart, NotesChart, UsersChart, @@ -610,6 +633,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PerUserDriveChart, ApRequestChart, ChartManagementService, + AbuseUserReportEntityService, AntennaEntityService, AppEntityService, @@ -642,6 +666,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting FlashEntityService, FlashLikeEntityService, RoleEntityService, + ReversiGameEntityService, + ReversiMatchingEntityService, + ApAudienceService, ApDbResolverService, ApDeliverManagerService, @@ -726,6 +753,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FanoutTimelineEndpointService, $ChannelFollowingService, $RegistryApiService, + $ReversiService, + $FederationChart, $NotesChart, $UsersChart, @@ -739,6 +768,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PerUserDriveChart, $ApRequestChart, $ChartManagementService, + $AbuseUserReportEntityService, $AntennaEntityService, $AppEntityService, @@ -771,6 +801,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, + $ReversiGameEntityService, + $ReversiMatchingEntityService, + $ApAudienceService, $ApDbResolverService, $ApDeliverManagerService, diff --git a/packages/backend/src/core/ReversiService.ts b/packages/backend/src/core/ReversiService.ts new file mode 100644 index 0000000000..64c0e25334 --- /dev/null +++ b/packages/backend/src/core/ReversiService.ts @@ -0,0 +1,126 @@ +/* + * 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 { In } from 'typeorm'; +import { ModuleRef } from '@nestjs/core'; +import type { + MiReversiGame, + MiRole, + MiRoleAssignment, + RoleAssignmentsRepository, + RolesRepository, + UsersRepository, +} from '@/models/_.js'; +import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.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 type { RoleCondFormulaValue } from '@/models/Role.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 { ModerationLogService } from '@/core/ModerationLogService.js'; +import type { Packed } from '@/misc/json-schema.js'; +import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; + +@Injectable() +export class ReversiService implements OnApplicationShutdown, OnModuleInit { + private notificationService: NotificationService; + + constructor( + private moduleRef: ModuleRef, + + @Inject(DI.redis) + private redisClient: Redis.Redis, + + @Inject(DI.redisForTimelines) + private redisForTimelines: Redis.Redis, + + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.rolesRepository) + private rolesRepository: RolesRepository, + + @Inject(DI.roleAssignmentsRepository) + private roleAssignmentsRepository: RoleAssignmentsRepository, + + private metaService: MetaService, + private cacheService: CacheService, + private userEntityService: UserEntityService, + private globalEventService: GlobalEventService, + private idService: IdService, + private moderationLogService: ModerationLogService, + private fanoutTimelineService: FanoutTimelineService, + ) { + } + + async onModuleInit() { + this.notificationService = this.moduleRef.get(NotificationService.name); + } + + @bindThis + public async getModerators(includeAdmins = true): Promise { + const ids = await this.getModeratorIds(includeAdmins); + const users = ids.length > 0 ? await this.usersRepository.findBy({ + id: In(ids), + }) : []; + return users; + } + + @bindThis + public async create(values: Partial, moderator?: MiUser): Promise { + const date = new Date(); + const created = await this.rolesRepository.insert({ + id: this.idService.gen(date.getTime()), + updatedAt: date, + lastUsedAt: date, + name: values.name, + description: values.description, + color: values.color, + iconUrl: values.iconUrl, + target: values.target, + condFormula: values.condFormula, + isPublic: values.isPublic, + isAdministrator: values.isAdministrator, + isModerator: values.isModerator, + isExplorable: values.isExplorable, + asBadge: values.asBadge, + canEditMembersByModerator: values.canEditMembersByModerator, + displayOrder: values.displayOrder, + policies: values.policies, + }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0])); + + this.globalEventService.publishInternalEvent('roleCreated', created); + + if (moderator) { + this.moderationLogService.log(moderator, 'createRole', { + roleId: created.id, + role: created, + }); + } + + return created; + } + + @bindThis + public dispose(): void { + } + + @bindThis + public onApplicationShutdown(signal?: string | undefined): void { + this.dispose(); + } +} diff --git a/packages/backend/src/core/entities/ReversiGameEntityService.ts b/packages/backend/src/core/entities/ReversiGameEntityService.ts new file mode 100644 index 0000000000..0bf02990d7 --- /dev/null +++ b/packages/backend/src/core/entities/ReversiGameEntityService.ts @@ -0,0 +1,81 @@ +/* + * 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 pack( + src: MiReversiGame['id'] | MiReversiGame, + me?: { id: MiUser['id'] } | null | undefined, + options?: { + detail?: boolean; + skipHide?: boolean; + }, + ): Promise> { + 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(), + isStarted: game.isStarted, + isEnded: game.isEnded, + form1: game.form1, + form2: game.form2, + user1Accepted: game.user1Accepted, + user2Accepted: game.user2Accepted, + 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, + surrendered: game.surrendered, + black: game.black, + bw: game.bw, + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + ...(options?.detail ? { + logs: game.logs.map(log => ({ + at: log.at.toISOString(), + color: log.color, + pos: log.pos, + })), + map: game.map, + } : {}), + }); + } + + @bindThis + public packMany( + xs: MiReversiGame[], + me?: { id: MiUser['id'] } | null | undefined, + ) { + return Promise.all(xs.map(x => this.pack(x, me))); + } +} + diff --git a/packages/backend/src/core/entities/ReversiMatchingEntityService.ts b/packages/backend/src/core/entities/ReversiMatchingEntityService.ts new file mode 100644 index 0000000000..1b39679ec1 --- /dev/null +++ b/packages/backend/src/core/entities/ReversiMatchingEntityService.ts @@ -0,0 +1,57 @@ +/* + * 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 { MiReversiMatching, ReversiMatchingsRepository } 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 { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; +import { UserEntityService } from './UserEntityService.js'; + +@Injectable() +export class ReversiMatchingEntityService { + constructor( + @Inject(DI.reversiMatchingsRepository) + private reversiMatchingsRepository: ReversiMatchingsRepository, + + private userEntityService: UserEntityService, + private idService: IdService, + ) { + } + + @bindThis + public async pack( + src: MiReversiMatching['id'] | MiReversiMatching, + me?: { id: MiUser['id'] } | null | undefined, + ): Promise> { + const matching = typeof src === 'object' ? src : await this.reversiMatchingsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: matching.id, + createdAt: this.idService.parse(matching.id).date.toISOString(), + parentId: matching.parentId, + parent: this.userEntityService.pack(matching.parentId, me, { + detail: true, + }), + childId: matching.childId, + child: this.userEntityService.pack(matching.childId, me, { + detail: true, + }), + }); + } + + @bindThis + public packMany( + xs: MiReversiMatching[], + me?: { id: MiUser['id'] } | null | undefined, + ) { + return Promise.all(xs.map(x => this.pack(x, me))); + } +} + diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 176978d35f..cabee95dad 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -39,6 +39,8 @@ 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 { packedReversiGameSchema } from '@/models/json-schema/reversi-game.js'; +import { packedReversiMatchingSchema } from '@/models/json-schema/reversi-matching.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -78,6 +80,8 @@ export const refs = { Signin: packedSigninSchema, RoleLite: packedRoleLiteSchema, Role: packedRoleSchema, + ReversiGame: packedReversiGameSchema, + ReversiMatching: packedReversiMatchingSchema, }; export type Packed = SchemaType; diff --git a/packages/backend/src/models/json-schema/reversi-game.ts b/packages/backend/src/models/json-schema/reversi-game.ts new file mode 100644 index 0000000000..fc7cdb063d --- /dev/null +++ b/packages/backend/src/models/json-schema/reversi-game.ts @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedReversiGameSchema = { + 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', + }, + 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, + }, + user1Accepted: { + type: 'boolean', + optional: false, nullable: false, + }, + user2Accepted: { + 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', + }, + surrendered: { + 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, + }, + logs: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'object', + optional: true, nullable: false, + properties: { + at: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + color: { + type: 'boolean', + optional: false, nullable: false, + }, + pos: { + type: 'number', + optional: false, nullable: false, + }, + }, + }, + }, + map: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, + }, +} as const; diff --git a/packages/backend/src/models/json-schema/reversi-matching.ts b/packages/backend/src/models/json-schema/reversi-matching.ts new file mode 100644 index 0000000000..a98b0a1644 --- /dev/null +++ b/packages/backend/src/models/json-schema/reversi-matching.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedReversiMatchingSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + parentId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + parent: { + type: 'object', + optional: false, nullable: true, + ref: 'User', + }, + childId: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + child: { + type: 'object', + optional: false, nullable: false, + ref: 'User', + }, + }, +} as const;