diff --git a/packages/backend/migration/1779921322355-world-avatar.js b/packages/backend/migration/1779921322355-world-avatar.js new file mode 100644 index 0000000000..00f577afea --- /dev/null +++ b/packages/backend/migration/1779921322355-world-avatar.js @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class WorldAvatar1779921322355 { + name = 'WorldAvatar1779921322355' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "world_avatar" ("id" character varying(32) NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "userId" character varying(32) NOT NULL, "def" jsonb NOT NULL DEFAULT '{}', "active" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_e7a27262285cc2c27114871f866" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_0f1d0bdfaca455cc2f13defabe" ON "world_avatar" ("updatedAt") `); + await queryRunner.query(`CREATE INDEX "IDX_4eba43c8e2540a92e99dd7f5a9" ON "world_avatar" ("userId") `); + await queryRunner.query(`ALTER TABLE "world_room" ADD "accessCount" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "world_avatar" ADD CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a" 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_avatar" DROP CONSTRAINT "FK_4eba43c8e2540a92e99dd7f5a9a"`); + await queryRunner.query(`ALTER TABLE "world_room" DROP COLUMN "accessCount"`); + await queryRunner.query(`DROP INDEX "public"."IDX_4eba43c8e2540a92e99dd7f5a9"`); + await queryRunner.query(`DROP INDEX "public"."IDX_0f1d0bdfaca455cc2f13defabe"`); + await queryRunner.query(`DROP TABLE "world_avatar"`); + } +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index cd68983d8d..8b1adccd0d 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -157,6 +157,8 @@ import { LoggerService } from './LoggerService.js'; import { WorldRoomService } from './WorldRoomService.js'; import { WorldRoomEntityService } from './entities/WorldRoomEntityService.js'; import { WorldRoomMultiplayService } from './WorldRoomMultiplayService.js'; +import { WorldAvatarService } from './WorldAvatarService.js'; +import { WorldAvatarEntityService } from './entities/WorldAvatarEntityService.js'; import type { Provider } from '@nestjs/common'; //#region 文字列ベースでのinjection用(循環参照対応のため) @@ -234,6 +236,7 @@ const $ReversiService: Provider = { provide: 'ReversiService', useExisting: Reve const $PageService: Provider = { provide: 'PageService', useExisting: PageService }; const $WorldRoomService: Provider = { provide: 'WorldRoomService', useExisting: WorldRoomService }; const $WorldRoomMultiplayService: Provider = { provide: 'WorldRoomMultiplayService', useExisting: WorldRoomMultiplayService }; +const $WorldAvatarService: Provider = { provide: 'WorldAvatarService', useExisting: WorldAvatarService }; const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService }; const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart }; @@ -290,6 +293,7 @@ const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService }; const $SystemWebhookEntityService: Provider = { provide: 'SystemWebhookEntityService', useExisting: SystemWebhookEntityService }; const $WorldRoomEntityService: Provider = { provide: 'WorldRoomEntityService', useExisting: WorldRoomEntityService }; +const $WorldAvatarEntityService: Provider = { provide: 'WorldAvatarEntityService', useExisting: WorldAvatarEntityService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; @@ -390,6 +394,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PageService, WorldRoomService, WorldRoomMultiplayService, + WorldAvatarService, ChartLoggerService, FederationChart, @@ -446,6 +451,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting MetaEntityService, SystemWebhookEntityService, WorldRoomEntityService, + WorldAvatarEntityService, ApAudienceService, ApDbResolverService, @@ -543,6 +549,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PageService, $WorldRoomService, $WorldRoomMultiplayService, + $WorldAvatarService, $ChartLoggerService, $FederationChart, @@ -599,6 +606,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $MetaEntityService, $SystemWebhookEntityService, $WorldRoomEntityService, + $WorldAvatarEntityService, $ApAudienceService, $ApDbResolverService, @@ -696,6 +704,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting PageService, WorldRoomService, WorldRoomMultiplayService, + WorldAvatarService, FederationChart, NotesChart, @@ -751,6 +760,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting MetaEntityService, SystemWebhookEntityService, WorldRoomEntityService, + WorldAvatarEntityService, ApAudienceService, ApDbResolverService, @@ -847,6 +857,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $PageService, $WorldRoomService, $WorldRoomMultiplayService, + $WorldAvatarService, $FederationChart, $NotesChart, @@ -902,6 +913,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $MetaEntityService, $SystemWebhookEntityService, $WorldRoomEntityService, + $WorldAvatarEntityService, $ApAudienceService, $ApDbResolverService, diff --git a/packages/backend/src/core/WorldAvatarService.ts b/packages/backend/src/core/WorldAvatarService.ts new file mode 100644 index 0000000000..d2eb5e422e --- /dev/null +++ b/packages/backend/src/core/WorldAvatarService.ts @@ -0,0 +1,130 @@ +/* + * 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 { + MiDriveFile, + MiWorldAvatar, +} from '@/models/_.js'; +import type { DriveFilesRepository, WorldAvatarsRepository } 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'; +import { QueryService } from '@/core/QueryService.js'; + +@Injectable() +export class WorldAvatarService { + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.worldAvatarsRepository) + private worldAvatarsRepository: WorldAvatarsRepository, + + private roleService: RoleService, + private moderationLogService: ModerationLogService, + private queryService: QueryService, + private idService: IdService, + ) { + } + + @bindThis + public async validateDef( + me: MiUser, + def: MiWorldAvatar['def'], + ): Promise { + // TODO + + return true; + } + + @bindThis + public async findMyAvatarById(userId: MiUser['id'], avatarId: MiWorldAvatar['id']) { + return this.worldAvatarsRepository.findOneBy({ id: avatarId, userId: userId }); + } + + @bindThis + public async findAvatarById(avatarId: MiWorldAvatar['id']) { + return this.worldAvatarsRepository.findOne({ where: { id: avatarId }, relations: ['user'] }); + } + + @bindThis + public async getMyAvatarsWithPagination(userId: MiUser['id'], limit: number, sinceId?: MiWorldAvatar['id'] | null, untilId?: MiWorldAvatar['id'] | null) { + const query = this.queryService.makePaginationQuery(this.worldAvatarsRepository.createQueryBuilder('avatar'), sinceId, untilId) + .andWhere('avatar.userId = :userId', { userId }); + + const avatars = await query.take(limit).getMany(); + + return avatars; + } + + @bindThis + public async create( + me: MiUser, + body: Partial, + ): Promise { + const currentAvatarsCount = await this.worldAvatarsRepository.countBy({ userId: me.id }); + + // TODO: limit by role policy + + const avatar = await this.worldAvatarsRepository.insertOne(new MiWorldAvatar({ + id: this.idService.gen(), + updatedAt: new Date(), + name: body.name, + def: body.def, + userId: me.id, + active: currentAvatarsCount === 0, + })); + + return avatar; + } + + @bindThis + public async update( + avatar: MiWorldAvatar, + body: Partial, + ): Promise { + body.updatedAt = new Date(); + const updated = await this.worldAvatarsRepository.createQueryBuilder().update() + .set(body) + .where('id = :id', { id: avatar.id }) + .returning('*') + .execute() + .then((response) => { + return response.raw[0]; + }); + + if (body.active) { + await this.worldAvatarsRepository.createQueryBuilder().update() + .set({ active: false }) + .where('userId = :userId', { userId: avatar.userId }) + .andWhere('id != :id', { id: avatar.id }) + .execute(); + } + + return updated; + } + + @bindThis + public async delete(avatar: MiWorldAvatar, deleter?: MiUser): Promise { + await this.worldAvatarsRepository.delete(avatar.id); + } + + @bindThis + public async getActiveAvatarOfUser(userId: MiUser['id']) { + return this.worldAvatarsRepository.findOneBy({ userId, active: true }); + } + + @bindThis + public async getActiveAvatarOfUsers(userIds: MiUser['id'][]): Promise { + if (userIds.length === 0) return []; + return this.worldAvatarsRepository.findBy({ userId: In(userIds), active: true }); + } +} diff --git a/packages/backend/src/core/entities/WorldAvatarEntityService.ts b/packages/backend/src/core/entities/WorldAvatarEntityService.ts new file mode 100644 index 0000000000..cf33d07563 --- /dev/null +++ b/packages/backend/src/core/entities/WorldAvatarEntityService.ts @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { DriveFilesRepository, MiWorldAvatar, WorldAvatarsRepository } 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 { MiDriveFile } from '@/models/DriveFile.js'; +import { bindThis } from '@/decorators.js'; +import { IdService } from '@/core/IdService.js'; +import { WorldAvatarService } from '@/core/WorldAvatarService.js'; +import { UserEntityService } from './UserEntityService.js'; +import { DriveFileEntityService } from './DriveFileEntityService.js'; +import { In } from 'typeorm'; + +@Injectable() +export class WorldAvatarEntityService { + constructor( + @Inject(DI.worldAvatarsRepository) + private worldAvatarsRepository: WorldAvatarsRepository, + + private worldAvatarService: WorldAvatarService, + private userEntityService: UserEntityService, + private idService: IdService, + ) { + } + + @bindThis + public async packLite( + src: MiWorldAvatar['id'] | MiWorldAvatar, + me?: { id: MiUser['id'] } | null | undefined, + hint?: { + packedUser?: Packed<'UserLite'> + }, + ): Promise> { + const meId = me ? me.id : null; + const avatar = typeof src === 'object' ? src : await this.worldAvatarsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: avatar.id, + def: avatar.def, + }); + } + + @bindThis + public async packDetailed( + src: MiWorldAvatar['id'] | MiWorldAvatar, + me?: { id: MiUser['id'] } | null | undefined, + hint?: { + packedUser?: Packed<'UserLite'> + }, + ): Promise> { + const meId = me ? me.id : null; + const avatar = typeof src === 'object' ? src : await this.worldAvatarsRepository.findOneByOrFail({ id: src }); + + return await awaitAll({ + id: avatar.id, + createdAt: this.idService.parse(avatar.id).date.toISOString(), + updatedAt: avatar.updatedAt.toISOString(), + name: avatar.name, + def: avatar.def, + active: avatar.active, + }); + } + + @bindThis + public async packLiteMany( + avatars: MiWorldAvatar[], + me?: { id: MiUser['id'] } | null | undefined, + ) { + const _users = avatars.map(({ user, userId }) => user ?? userId); + const _userMap = await this.userEntityService.packMany(_users, me) + .then(users => new Map(users.map(u => [u.id, u]))); + return Promise.all(avatars.map(avatar => this.packLite(avatar, me, { packedUser: _userMap.get(avatar.userId) }))); + } +} + diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index 4f81281292..1362286df8 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -92,5 +92,6 @@ export const DI = { reversiGamesRepository: Symbol('reversiGamesRepository'), noteDraftsRepository: Symbol('noteDraftsRepository'), worldRoomsRepository: Symbol('worldRoomsRepository'), + worldAvatarsRepository: Symbol('worldAvatarsRepository'), //#endregion }; diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 7c5ad33cbe..abe0fb8d3c 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -76,6 +76,7 @@ import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-m import { packedAchievementNameSchema, packedAchievementSchema } from '@/models/json-schema/achievement.js'; import { packedNoteDraftSchema } from '@/models/json-schema/note-draft.js'; import { packedWorldRoomDetailedSchema, packedWorldRoomLiteSchema } from '@/models/json-schema/world-room.js'; +import { packedWorldAvatarDetailedSchema, packedWorldAvatarLiteSchema } from '@/models/json-schema/world-avatar.js'; export const refs = { UserLite: packedUserLiteSchema, @@ -150,6 +151,8 @@ export const refs = { ChatRoomMembership: packedChatRoomMembershipSchema, WorldRoomLite: packedWorldRoomLiteSchema, WorldRoomDetailed: packedWorldRoomDetailedSchema, + WorldAvatarLite: packedWorldAvatarLiteSchema, + WorldAvatarDetailed: packedWorldAvatarDetailedSchema, }; export type Packed = SchemaType; diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 49e465bf5b..1d2716bf37 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -85,6 +85,7 @@ import { MiChatRoomInvitation, MiChatApproval, MiWorldRoom, + MiWorldAvatar, } from './_.js'; import type { Provider } from '@nestjs/common'; import type { DataSource } from 'typeorm'; @@ -551,6 +552,12 @@ const $worldRoomsRepository: Provider = { inject: [DI.db], }; +const $worldAvatarsRepository: Provider = { + provide: DI.worldAvatarsRepository, + useFactory: (db: DataSource) => db.getRepository(MiWorldAvatar).extend(miRepository as MiRepository), + inject: [DI.db], +}; + @Module({ imports: [], providers: [ @@ -631,6 +638,7 @@ const $worldRoomsRepository: Provider = { $bubbleGameRecordsRepository, $reversiGamesRepository, $worldRoomsRepository, + $worldAvatarsRepository, ], exports: [ $usersRepository, @@ -710,6 +718,7 @@ const $worldRoomsRepository: Provider = { $bubbleGameRecordsRepository, $reversiGamesRepository, $worldRoomsRepository, + $worldAvatarsRepository, ], }) export class RepositoryModule { diff --git a/packages/backend/src/models/WorldAvatar.ts b/packages/backend/src/models/WorldAvatar.ts new file mode 100644 index 0000000000..856b8401c2 --- /dev/null +++ b/packages/backend/src/models/WorldAvatar.ts @@ -0,0 +1,54 @@ +/* + * 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'; + +@Entity('world_avatar') +export class MiWorldAvatar { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + }) + public updatedAt: Date; + + @Column('varchar', { + length: 256, + }) + public name: string; + + @Index() + @Column({ + ...id(), + }) + public userId: MiUser['id']; + + @ManyToOne(() => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column('boolean', { + default: false, + }) + public active: boolean; + + @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/WorldRoom.ts b/packages/backend/src/models/WorldRoom.ts index ba239a51e2..12a8fc6668 100644 --- a/packages/backend/src/models/WorldRoom.ts +++ b/packages/backend/src/models/WorldRoom.ts @@ -47,6 +47,11 @@ export class MiWorldRoom { }) public likedCount: number; + @Column('integer', { + default: 0, + }) + public accessCount: number; + @Column('varchar', { length: 128, default: 'public', }) diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index e393bdad51..a9df42dd27 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -85,6 +85,7 @@ 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 { MiWorldAvatar } from '@/models/WorldAvatar.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; export interface MiRepository { @@ -175,6 +176,7 @@ export { MiBubbleGameRecord, MiReversiGame, MiWorldRoom, + MiWorldAvatar, }; export type AbuseUserReportsRepository = Repository & MiRepository; @@ -256,3 +258,4 @@ export type ChatApprovalsRepository = Repository & MiRepository< export type BubbleGameRecordsRepository = Repository & MiRepository; export type ReversiGamesRepository = Repository & MiRepository; export type WorldRoomsRepository = Repository & MiRepository; +export type WorldAvatarsRepository = Repository & MiRepository; diff --git a/packages/backend/src/models/json-schema/world-avatar.ts b/packages/backend/src/models/json-schema/world-avatar.ts new file mode 100644 index 0000000000..72bd228788 --- /dev/null +++ b/packages/backend/src/models/json-schema/world-avatar.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const packedWorldAvatarLiteSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + def: { + type: 'object', + optional: false, nullable: false, + }, + }, +} as const; + +export const packedWorldAvatarDetailedSchema = { + type: 'object', + properties: { + id: { + type: 'string', + optional: false, nullable: false, + format: 'id', + }, + createdAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + updatedAt: { + type: 'string', + optional: false, nullable: false, + format: 'date-time', + }, + name: { + type: 'string', + optional: false, nullable: false, + }, + def: { + type: 'object', + optional: false, nullable: false, + }, + active: { + type: 'boolean', + optional: false, nullable: false, + }, + }, +} as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index b91ec4ae50..8794288c7d 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -88,6 +88,7 @@ import { MiReversiGame } from '@/models/ReversiGame.js'; import { MiChatApproval } from '@/models/ChatApproval.js'; import { MiSystemAccount } from '@/models/SystemAccount.js'; import { MiWorldRoom } from '@/models/WorldRoom.js'; +import { MiWorldAvatar } from '@/models/WorldAvatar.js'; pg.types.setTypeParser(20, Number); @@ -256,6 +257,7 @@ export const entities = [ MiBubbleGameRecord, MiReversiGame, MiWorldRoom, + MiWorldAvatar, ...charts, ]; diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 3c00e9b18b..4ad9286922 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -448,4 +448,9 @@ export * as 'world/rooms/update' from './endpoints/world/rooms/update.js'; export * as 'world/rooms/delete' from './endpoints/world/rooms/delete.js'; export * as 'world/rooms/list-by-user' from './endpoints/world/rooms/list-by-user.js'; export * as 'world/rooms/show' from './endpoints/world/rooms/show.js'; +export * as 'world/avatars/create' from './endpoints/world/avatars/create.js'; +export * as 'world/avatars/update' from './endpoints/world/avatars/update.js'; +export * as 'world/avatars/delete' from './endpoints/world/avatars/delete.js'; +export * as 'world/avatars/list' from './endpoints/world/avatars/list.js'; +export * as 'world/avatars/show' from './endpoints/world/avatars/show.js'; export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js'; diff --git a/packages/backend/src/server/api/endpoints/world/avatars/create.ts b/packages/backend/src/server/api/endpoints/world/avatars/create.ts new file mode 100644 index 0000000000..bb5f6c30f5 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/world/avatars/create.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * 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 { DI } from '@/di-symbols.js'; +import { ApiError } from '@/server/api/error.js'; +import { WorldAvatarService } from '@/core/WorldAvatarService.js'; +import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js'; + +export const meta = { + tags: ['worldAvatar'], + + requireCredential: true, + + prohibitMoved: true, + + kind: 'write:worldAvatar', + + limit: { + duration: ms('1day'), + max: 10, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'WorldAvatarDetailed', + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { type: 'string', maxLength: 256 }, + def: { type: 'object', additionalProperties: true }, + }, + required: ['name', 'def'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private worldAvatarService: WorldAvatarService, + private worldAvatarEntityService: WorldAvatarEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + // TODO: validate avatar + + const avatar = await this.worldAvatarService.create(me, { + name: ps.name, + def: ps.def, + }); + return await this.worldAvatarEntityService.packDetailed(avatar); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/world/avatars/delete.ts b/packages/backend/src/server/api/endpoints/world/avatars/delete.ts new file mode 100644 index 0000000000..405f82db13 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/world/avatars/delete.ts @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { WorldAvatarService } from '@/core/WorldAvatarService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['worldAvatar'], + + requireCredential: true, + + kind: 'write:worldAvatar', + + errors: { + noSuchAvatar: { + message: 'No such avatar.', + code: 'NO_SUCH_ROOM', + id: 'd4e3753d-97bf-4a19-ab8e-21080fbc0f4c', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + avatarId: { type: 'string', format: 'misskey:id' }, + }, + required: ['avatarId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private worldAvatarService: WorldAvatarService, + ) { + super(meta, paramDef, async (ps, me) => { + const avatar = await this.worldAvatarService.findMyAvatarById(me.id, ps.avatarId); + if (avatar == null) { + throw new ApiError(meta.errors.noSuchAvatar); + } + + await this.worldAvatarService.delete(avatar, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/world/avatars/list.ts b/packages/backend/src/server/api/endpoints/world/avatars/list.ts new file mode 100644 index 0000000000..91b0e4ae68 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/world/avatars/list.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { WorldAvatarService } from '@/core/WorldAvatarService.js'; +import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js'; +import { ApiError } from '@/server/api/error.js'; +import { IdService } from '@/core/IdService.js'; + +export const meta = { + tags: ['worldAvatar'], + + requireCredential: true, + + kind: 'read:worldAvatar', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'WorldAvatarLite', + }, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + }, + required: ['userId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private worldAvatarEntityService: WorldAvatarEntityService, + private worldAvatarService: WorldAvatarService, + private idService: IdService, + ) { + super(meta, paramDef, async (ps, me) => { + const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); + const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); + + const avatars = await this.worldAvatarService.getMyAvatarsWithPagination(ps.userId, ps.limit, sinceId, untilId); + return this.worldAvatarEntityService.packLiteMany(avatars, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/world/avatars/show.ts b/packages/backend/src/server/api/endpoints/world/avatars/show.ts new file mode 100644 index 0000000000..0a33d8013c --- /dev/null +++ b/packages/backend/src/server/api/endpoints/world/avatars/show.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { WorldAvatarService } from '@/core/WorldAvatarService.js'; +import { ApiError } from '@/server/api/error.js'; +import { WorldAvatarEntityService } from '@/core/entities/WorldAvatarEntityService.js'; + +export const meta = { + tags: ['worldAvatar'], + + requireCredential: true, + + kind: 'read:worldAvatar', + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'WorldAvatarDetailed', + }, + + errors: { + noSuchAvatar: { + message: 'No such avatar.', + code: 'NO_SUCH_ROOM', + id: '857ae02f-8759-4d20-9adb-6e95fffe4fd8', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + avatarId: { type: 'string', format: 'misskey:id' }, + }, + required: ['avatarId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private worldAvatarService: WorldAvatarService, + private worldAvatarEntityService: WorldAvatarEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const avatar = await this.worldAvatarService.findAvatarById(ps.avatarId); + if (avatar == null) { + throw new ApiError(meta.errors.noSuchAvatar); + } + + if (avatar.userId !== me.id) { + throw new ApiError(meta.errors.noSuchAvatar); + } + + return this.worldAvatarEntityService.packDetailed(avatar, me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/world/avatars/update.ts b/packages/backend/src/server/api/endpoints/world/avatars/update.ts new file mode 100644 index 0000000000..87896b9970 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/world/avatars/update.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { WorldAvatarService } from '@/core/WorldAvatarService.js'; +import { ApiError } from '@/server/api/error.js'; + +export const meta = { + tags: ['worldAvatar'], + + requireCredential: true, + + kind: 'write:worldAvatar', + + res: { + }, + + errors: { + noSuchAvatar: { + message: 'No such avatar.', + code: 'NO_SUCH_ROOM', + id: 'fcdb0f92-bda6-47f9-bd05-343e0e020933', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + avatarId: { type: 'string', format: 'misskey:id' }, + name: { type: 'string', maxLength: 256 }, + def: { type: 'object', additionalProperties: true }, + active: { type: 'boolean' }, + }, + required: ['avatarId'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + private worldAvatarService: WorldAvatarService, + ) { + super(meta, paramDef, async (ps, me) => { + const avatar = await this.worldAvatarService.findMyAvatarById(me.id, ps.avatarId); + if (avatar == null) { + throw new ApiError(meta.errors.noSuchAvatar); + } + + // TODO: validate avatar + + await this.worldAvatarService.update(avatar, { + name: ps.name, + def: ps.def, + active: ps.active, + }); + }); + } +} diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 01b61c214b..6d030d4174 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -924,6 +924,27 @@ export type Channels = { }; }; }; + worldRoom: { + params: { + roomId: string; + }; + events: { + entered: (payload: { + playerProfiles: any; + }) => void; + sync: (payload: any) => void; + playerEntered: (payload: { + id: string; + profile: any; + }) => void; + playerLeft: (payload: { + id: string; + }) => void; + }; + receives: { + update: any; + }; + }; }; // @public (undocumented) @@ -2183,6 +2204,15 @@ declare namespace entities { V2AdminEmojiListRequest, V2AdminEmojiListResponse, VerifyEmailRequest, + WorldAvatarsCreateRequest, + WorldAvatarsCreateResponse, + WorldAvatarsDeleteRequest, + WorldAvatarsListRequest, + WorldAvatarsListResponse, + WorldAvatarsShowRequest, + WorldAvatarsShowResponse, + WorldAvatarsUpdateRequest, + WorldAvatarsUpdateResponse, WorldRoomsCreateRequest, WorldRoomsCreateResponse, WorldRoomsDeleteRequest, @@ -2263,7 +2293,9 @@ declare namespace entities { ChatRoomInvitation, ChatRoomMembership, WorldRoomLite, - WorldRoomDetailed + WorldRoomDetailed, + WorldAvatarLite, + WorldAvatarDetailed } } export { entities } @@ -3912,6 +3944,39 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons // @public (undocumented) type VerifyEmailRequest = operations['verify-email']['requestBody']['content']['application/json']; +// @public (undocumented) +type WorldAvatarDetailed = components['schemas']['WorldAvatarDetailed']; + +// @public (undocumented) +type WorldAvatarLite = components['schemas']['WorldAvatarLite']; + +// @public (undocumented) +type WorldAvatarsCreateRequest = operations['world___avatars___create']['requestBody']['content']['application/json']; + +// @public (undocumented) +type WorldAvatarsCreateResponse = operations['world___avatars___create']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type WorldAvatarsDeleteRequest = operations['world___avatars___delete']['requestBody']['content']['application/json']; + +// @public (undocumented) +type WorldAvatarsListRequest = operations['world___avatars___list']['requestBody']['content']['application/json']; + +// @public (undocumented) +type WorldAvatarsListResponse = operations['world___avatars___list']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type WorldAvatarsShowRequest = operations['world___avatars___show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type WorldAvatarsShowResponse = operations['world___avatars___show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type WorldAvatarsUpdateRequest = operations['world___avatars___update']['requestBody']['content']['application/json']; + +// @public (undocumented) +type WorldAvatarsUpdateResponse = operations['world___avatars___update']['responses']['200']['content']['application/json']; + // @public (undocumented) type WorldRoomDetailed = components['schemas']['WorldRoomDetailed']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 5bfdfb3b21..bd22444b74 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -4829,6 +4829,61 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:worldAvatar* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:worldAvatar* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:worldAvatar* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:worldAvatar* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:worldAvatar* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 3e658d0c87..8c8e37a082 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -661,6 +661,15 @@ import type { V2AdminEmojiListRequest, V2AdminEmojiListResponse, VerifyEmailRequest, + WorldAvatarsCreateRequest, + WorldAvatarsCreateResponse, + WorldAvatarsDeleteRequest, + WorldAvatarsListRequest, + WorldAvatarsListResponse, + WorldAvatarsShowRequest, + WorldAvatarsShowResponse, + WorldAvatarsUpdateRequest, + WorldAvatarsUpdateResponse, WorldRoomsCreateRequest, WorldRoomsCreateResponse, WorldRoomsDeleteRequest, @@ -1107,6 +1116,11 @@ export type Endpoints = { 'users/update-memo': { req: UsersUpdateMemoRequest; res: EmptyResponse }; 'v2/admin/emoji/list': { req: V2AdminEmojiListRequest; res: V2AdminEmojiListResponse }; 'verify-email': { req: VerifyEmailRequest; res: EmptyResponse }; + 'world/avatars/create': { req: WorldAvatarsCreateRequest; res: WorldAvatarsCreateResponse }; + 'world/avatars/delete': { req: WorldAvatarsDeleteRequest; res: EmptyResponse }; + 'world/avatars/list': { req: WorldAvatarsListRequest; res: WorldAvatarsListResponse }; + 'world/avatars/show': { req: WorldAvatarsShowRequest; res: WorldAvatarsShowResponse }; + 'world/avatars/update': { req: WorldAvatarsUpdateRequest; res: WorldAvatarsUpdateResponse }; 'world/rooms/create': { req: WorldRoomsCreateRequest; res: WorldRoomsCreateResponse }; 'world/rooms/delete': { req: WorldRoomsDeleteRequest; res: EmptyResponse }; 'world/rooms/list-by-user': { req: WorldRoomsListByUserRequest; res: WorldRoomsListByUserResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index ab2979ead9..4d0dc2b417 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -664,6 +664,15 @@ export type UsersUpdateMemoRequest = operations['users___update-memo']['requestB export type V2AdminEmojiListRequest = operations['v2___admin___emoji___list']['requestBody']['content']['application/json']; export type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['responses']['200']['content']['application/json']; export type VerifyEmailRequest = operations['verify-email']['requestBody']['content']['application/json']; +export type WorldAvatarsCreateRequest = operations['world___avatars___create']['requestBody']['content']['application/json']; +export type WorldAvatarsCreateResponse = operations['world___avatars___create']['responses']['200']['content']['application/json']; +export type WorldAvatarsDeleteRequest = operations['world___avatars___delete']['requestBody']['content']['application/json']; +export type WorldAvatarsListRequest = operations['world___avatars___list']['requestBody']['content']['application/json']; +export type WorldAvatarsListResponse = operations['world___avatars___list']['responses']['200']['content']['application/json']; +export type WorldAvatarsShowRequest = operations['world___avatars___show']['requestBody']['content']['application/json']; +export type WorldAvatarsShowResponse = operations['world___avatars___show']['responses']['200']['content']['application/json']; +export type WorldAvatarsUpdateRequest = operations['world___avatars___update']['requestBody']['content']['application/json']; +export type WorldAvatarsUpdateResponse = operations['world___avatars___update']['responses']['200']['content']['application/json']; export type WorldRoomsCreateRequest = operations['world___rooms___create']['requestBody']['content']['application/json']; export type WorldRoomsCreateResponse = operations['world___rooms___create']['responses']['200']['content']['application/json']; export type WorldRoomsDeleteRequest = operations['world___rooms___delete']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts index 818f9d9a51..d724f257dc 100644 --- a/packages/misskey-js/src/autogen/models.ts +++ b/packages/misskey-js/src/autogen/models.ts @@ -71,3 +71,5 @@ export type ChatRoomInvitation = components['schemas']['ChatRoomInvitation']; export type ChatRoomMembership = components['schemas']['ChatRoomMembership']; export type WorldRoomLite = components['schemas']['WorldRoomLite']; export type WorldRoomDetailed = components['schemas']['WorldRoomDetailed']; +export type WorldAvatarLite = components['schemas']['WorldAvatarLite']; +export type WorldAvatarDetailed = components['schemas']['WorldAvatarDetailed']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 6e18820b38..4156ef6ff7 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3960,6 +3960,51 @@ export type paths = { */ post: operations['verify-email']; }; + '/world/avatars/create': { + /** + * world/avatars/create + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:worldAvatar* + */ + post: operations['world___avatars___create']; + }; + '/world/avatars/delete': { + /** + * world/avatars/delete + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:worldAvatar* + */ + post: operations['world___avatars___delete']; + }; + '/world/avatars/list': { + /** + * world/avatars/list + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:worldAvatar* + */ + post: operations['world___avatars___list']; + }; + '/world/avatars/show': { + /** + * world/avatars/show + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:worldAvatar* + */ + post: operations['world___avatars___show']; + }; + '/world/avatars/update': { + /** + * world/avatars/update + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:worldAvatar* + */ + post: operations['world___avatars___update']; + }; '/world/rooms/create': { /** * world/rooms/create @@ -5761,6 +5806,22 @@ export type components = { def: Record; attachedFiles: components['schemas']['DriveFile'][]; }; + WorldAvatarLite: { + /** Format: id */ + id: string; + def: Record; + }; + WorldAvatarDetailed: { + /** Format: id */ + id: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + name: string; + def: Record; + active: boolean; + }; }; responses: never; parameters: never; @@ -37023,6 +37084,355 @@ export interface operations { }; }; }; + world___avatars___create: { + requestBody: { + content: { + 'application/json': { + name: string; + def: { + [key: string]: unknown; + }; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['WorldAvatarDetailed']; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Too many requests */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + world___avatars___delete: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + avatarId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + headers: { + [name: string]: unknown; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + world___avatars___list: { + requestBody: { + content: { + 'application/json': { + /** @default 30 */ + limit?: number; + /** Format: misskey:id */ + sinceId?: string; + /** Format: misskey:id */ + untilId?: string; + sinceDate?: number; + untilDate?: number; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['WorldAvatarLite'][]; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + world___avatars___show: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + avatarId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['WorldAvatarDetailed']; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + world___avatars___update: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + avatarId: string; + name?: string; + def?: { + [key: string]: unknown; + }; + active?: boolean; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': unknown; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; world___rooms___create: { requestBody: { content: { diff --git a/packages/misskey-world/src/types.ts b/packages/misskey-world/src/types.ts new file mode 100644 index 0000000000..4ea7794a5f --- /dev/null +++ b/packages/misskey-world/src/types.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export type WorldAvatar = { + type: 'default'; + body: { + color: [number, number, number]; + roughness: number; + metallic: number; + }; + eyes: { + type: string; + color: [number, number, number]; + }; + mouth: { + type: string; + color: [number, number, number]; + }; + accessories: { + type: string; + }[]; +};