From 03481322a9a9d9ba7feab0eaa89b58550c92cbd8 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 14 May 2026 17:10:28 +0900 Subject: [PATCH] wip --- .../migration/1778744540138-world-room.js | 28 ++++++ packages/backend/src/core/WorldRoomService.ts | 93 +++++++++++++++++++ packages/backend/src/di-symbols.ts | 1 + .../backend/src/models/RepositoryModule.ts | 9 ++ packages/backend/src/models/WorldRoom.ts | 68 ++++++++++++++ packages/backend/src/models/_.ts | 5 +- packages/backend/src/postgres.ts | 2 + 7 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 packages/backend/migration/1778744540138-world-room.js create mode 100644 packages/backend/src/core/WorldRoomService.ts create mode 100644 packages/backend/src/models/WorldRoom.ts diff --git a/packages/backend/migration/1778744540138-world-room.js b/packages/backend/migration/1778744540138-world-room.js new file mode 100644 index 0000000000..9243c2a9ee --- /dev/null +++ b/packages/backend/migration/1778744540138-world-room.js @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class WorldRoom1778744540138 { + name = 'WorldRoom1778744540138' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "world_room" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(1024) NOT NULL, "userId" character varying(32) NOT NULL, "likedCount" integer NOT NULL DEFAULT '0', "visibility" character varying(128) NOT NULL DEFAULT 'public', "def" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_40cfacaf35b0b54bb2281c89767" PRIMARY KEY ("id")); COMMENT ON COLUMN "world_room"."userId" IS 'The ID of author.'`); + await queryRunner.query(`CREATE INDEX "IDX_88289375952050da4a7752a366" ON "world_room" ("updatedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_f803a5efb4125c5fd8a414285e" ON "world_room" ("userId") `); + await queryRunner.query(`ALTER TABLE "world_room" ADD CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "world_room" DROP CONSTRAINT "FK_f803a5efb4125c5fd8a414285ed"`); + await queryRunner.query(`DROP INDEX "public"."IDX_f803a5efb4125c5fd8a414285e"`); + await queryRunner.query(`DROP INDEX "public"."IDX_88289375952050da4a7752a366"`); + await queryRunner.query(`DROP TABLE "world_room"`); + } +} diff --git a/packages/backend/src/core/WorldRoomService.ts b/packages/backend/src/core/WorldRoomService.ts new file mode 100644 index 0000000000..00d20c5b3b --- /dev/null +++ b/packages/backend/src/core/WorldRoomService.ts @@ -0,0 +1,93 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, In, Not } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import { + MiPage, + MiDriveFile, + type UsersRepository, + WorldRoomsRepository, + MiWorldRoom, +} from '@/models/_.js'; +import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; +import { IdService } from '@/core/IdService.js'; +import type { MiUser } from '@/models/User.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; + +@Injectable() +export class WorldRoomService { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.worldRoomsRepository) + private worldRoomsRepository: WorldRoomsRepository, + + private roleService: RoleService, + private moderationLogService: ModerationLogService, + private idService: IdService, + ) { + } + + @bindThis + public async create( + me: MiUser, + body: Partial, + ): Promise { + const room = await this.worldRoomsRepository.insertOne(new MiWorldRoom({ + id: this.idService.gen(), + updatedAt: new Date(), + name: body.name, + description: body.description, + def: body.def, + userId: me.id, + visibility: 'public', + })); + + return room; + } + + @bindThis + public async update( + me: MiUser, + roomId: MiWorldRoom['id'], + body: Partial, + ): Promise { + return this.worldRoomsRepository.createQueryBuilder().update() + .set(body) + .where('id = :id', { id: roomId }) + .returning('*') + .execute() + .then((response) => { + return response.raw[0]; + }); + } + + @bindThis + public async delete(me: MiUser, roomId: MiWorldRoom['id']): Promise { + await this.worldRoomsRepository.delete(roomId); + } + + // TODO: engineの実装と共通化 + collectReferencedDriveFileIds(roomState: MiWorldRoom['def']): Set { + const fileIds = new Set(); + for (const o of roomState.installedObjects) { + const def = getObjectDef(o.type); // 家具定義をバックエンドで利用するにはcreateInstance定義を分離し、それ以外を別パッケージとして抽出する必要がありそう しかしそれ以外の定義についてもi18nされたlabelなど、バックエンドには相応しくない情報も含まれているのでどうするか + for (const schemaRecord of Object.entries(def.options.schema)) { + if (schemaRecord[1].type === 'file') { + const optionValue = o.options[schemaRecord[0]]; + if (optionValue != null && optionValue !== '') { + fileIds.add(optionValue); + } + } + } + } + return fileIds; + } +} diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index b9ca76233c..4f81281292 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -91,5 +91,6 @@ export const DI = { bubbleGameRecordsRepository: Symbol('bubbleGameRecordsRepository'), reversiGamesRepository: Symbol('reversiGamesRepository'), noteDraftsRepository: Symbol('noteDraftsRepository'), + worldRoomsRepository: Symbol('worldRoomsRepository'), //#endregion }; diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index e3db6f8838..49e465bf5b 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -84,6 +84,7 @@ import { MiChatRoomMembership, MiChatRoomInvitation, MiChatApproval, + MiWorldRoom, } from './_.js'; import type { Provider } from '@nestjs/common'; import type { DataSource } from 'typeorm'; @@ -544,6 +545,12 @@ const $reversiGamesRepository: Provider = { inject: [DI.db], }; +const $worldRoomsRepository: Provider = { + provide: DI.worldRoomsRepository, + useFactory: (db: DataSource) => db.getRepository(MiWorldRoom).extend(miRepository as MiRepository), + inject: [DI.db], +}; + @Module({ imports: [], providers: [ @@ -623,6 +630,7 @@ const $reversiGamesRepository: Provider = { $chatApprovalsRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, + $worldRoomsRepository, ], exports: [ $usersRepository, @@ -701,6 +709,7 @@ const $reversiGamesRepository: Provider = { $chatApprovalsRepository, $bubbleGameRecordsRepository, $reversiGamesRepository, + $worldRoomsRepository, ], }) export class RepositoryModule { diff --git a/packages/backend/src/models/WorldRoom.ts b/packages/backend/src/models/WorldRoom.ts new file mode 100644 index 0000000000..0407ef1fd6 --- /dev/null +++ b/packages/backend/src/models/WorldRoom.ts @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +export const worldRoomVisibility = ['public', 'private'] as const; +export type WorldRoomVisibility = typeof worldRoomVisibility[number]; + +@Entity('world_room') +export class MiWorldRoom { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + }) + public updatedAt: Date; + + @Column('varchar', { + length: 256, + }) + public name: string; + + @Column('varchar', { + length: 1024, + }) + public description: string; + + @Index() + @Column({ + ...id(), + comment: 'The ID of author.', + }) + public userId: MiUser['id']; + + @ManyToOne(() => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column('integer', { + default: 0, + }) + public likedCount: number; + + @Column('varchar', { + length: 128, default: 'public', + }) + public visibility: WorldRoomVisibility; + + @Column('jsonb', { + default: {}, + }) + public def: Record; + + constructor(data: Partial) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index c4528e3a77..e393bdad51 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -23,7 +23,7 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiChannel } from '@/models/Channel.js'; import { MiChannelFavorite } from '@/models/ChannelFavorite.js'; import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; -import { MiChannelMuting } from "@/models/ChannelMuting.js"; +import { MiChannelMuting } from '@/models/ChannelMuting.js'; import { MiChatApproval } from '@/models/ChatApproval.js'; import { MiChatMessage } from '@/models/ChatMessage.js'; import { MiChatRoom } from '@/models/ChatRoom.js'; @@ -84,6 +84,7 @@ import { MiUserProfile } from '@/models/UserProfile.js'; import { MiUserPublickey } from '@/models/UserPublickey.js'; import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; import { MiWebhook } from '@/models/Webhook.js'; +import { MiWorldRoom } from '@/models/WorldRoom.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; export interface MiRepository { @@ -173,6 +174,7 @@ export { MiChatApproval, MiBubbleGameRecord, MiReversiGame, + MiWorldRoom, }; export type AbuseUserReportsRepository = Repository & MiRepository; @@ -253,3 +255,4 @@ export type ChatRoomInvitationsRepository = Repository & M export type ChatApprovalsRepository = Repository & MiRepository; export type BubbleGameRecordsRepository = Repository & MiRepository; export type ReversiGamesRepository = Repository & MiRepository; +export type WorldRoomsRepository = Repository & MiRepository; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 3dcd3f0965..b91ec4ae50 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -87,6 +87,7 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; import { MiChatApproval } from '@/models/ChatApproval.js'; import { MiSystemAccount } from '@/models/SystemAccount.js'; +import { MiWorldRoom } from '@/models/WorldRoom.js'; pg.types.setTypeParser(20, Number); @@ -254,6 +255,7 @@ export const entities = [ MiChatApproval, MiBubbleGameRecord, MiReversiGame, + MiWorldRoom, ...charts, ];