From 9935d264e2e0b543097bb093a0d55963cfa656f5 Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Wed, 14 Aug 2024 22:10:03 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat(profile):=20=E7=9B=B8=E4=BA=92?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF=E6=A9=9F=E8=83=BD=E3=81=AE=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=20(MisskeyIO#675)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit b6a5a36eaa66883e9306c782b71cb08f80cb12bc) # Conflicts: # locales/index.d.ts # locales/ja-JP.yml # packages/backend/src/core/CoreModule.ts # packages/backend/src/core/entities/UserEntityService.ts # packages/backend/src/models/RepositoryModule.ts # packages/backend/src/models/_.ts # packages/backend/src/models/json-schema/user.ts # packages/backend/src/server/api/endpoints/i/update.ts # packages/backend/src/types.ts # packages/cherrypick-js/etc/cherrypick-js.api.md # packages/cherrypick-js/src/autogen/endpoint.ts # packages/cherrypick-js/src/autogen/entities.ts # packages/cherrypick-js/src/consts.ts # packages/frontend/src/pages/admin-user.vue # packages/frontend/src/pages/user/home.vue # packages/frontend/src/pages/user/index.timeline.vue Co-authored-by: まっちゃてぃー。 <56515516+mattyatea@users.noreply.github.com> --- locales/index.d.ts | 40 ++++++ locales/ja-JP.yml | 10 ++ .../migration/1723213482131-mutualBanner.js | 27 ++++ packages/backend/src/core/CoreModule.ts | 24 ++++ .../src/core/UserBannerPiningService.ts | 55 +++++++++ .../backend/src/core/UserBannerService.ts | 115 ++++++++++++++++++ .../core/entities/UserBannerEntityService.ts | 56 +++++++++ .../entities/UserBannerPiningEntityService.ts | 23 ++++ .../src/core/entities/UserEntityService.ts | 25 +++- packages/backend/src/di-symbols.ts | 2 + packages/backend/src/misc/json-schema.ts | 2 + .../backend/src/models/RepositoryModule.ts | 20 ++- packages/backend/src/models/UserBanner.ts | 42 +++++++ .../backend/src/models/UserBannerPining.ts | 32 +++++ packages/backend/src/models/UserProfile.ts | 2 +- packages/backend/src/models/_.ts | 4 + .../backend/src/models/json-schema/user.ts | 112 +++++++++++++++++ packages/backend/src/postgres.ts | 4 + .../backend/src/server/api/EndpointsModule.ts | 4 + .../backend/src/server/api/endpoint-base.ts | 11 ++ packages/backend/src/server/api/endpoints.ts | 2 + .../admin/unset-user-mutual-banner.ts | 58 +++++++++ .../src/server/api/endpoints/i/update.ts | 93 +++++++++++++- packages/backend/src/types.ts | 8 ++ packages/backend/test/e2e/users.ts | 2 + .../cherrypick-js/etc/cherrypick-js.api.md | 12 +- .../src/autogen/apiClientJSDoc.ts | 11 ++ .../cherrypick-js/src/autogen/endpoint.ts | 3 + .../cherrypick-js/src/autogen/entities.ts | 1 + packages/cherrypick-js/src/autogen/models.ts | 1 + packages/cherrypick-js/src/autogen/types.ts | 105 ++++++++++++++++ packages/cherrypick-js/src/consts.ts | 9 ++ packages/frontend/src/components/MkLink.vue | 4 +- packages/frontend/src/pages/admin-user.vue | 15 +++ .../frontend/src/pages/settings/profile.vue | 83 +++++++++++++ packages/frontend/src/pages/user/home.vue | 64 ++++++++++ .../src/pages/user/index.timeline.vue | 49 +++++++- 37 files changed, 1122 insertions(+), 8 deletions(-) create mode 100644 packages/backend/migration/1723213482131-mutualBanner.js create mode 100644 packages/backend/src/core/UserBannerPiningService.ts create mode 100644 packages/backend/src/core/UserBannerService.ts create mode 100644 packages/backend/src/core/entities/UserBannerEntityService.ts create mode 100644 packages/backend/src/core/entities/UserBannerPiningEntityService.ts create mode 100644 packages/backend/src/models/UserBanner.ts create mode 100644 packages/backend/src/models/UserBannerPining.ts create mode 100644 packages/backend/src/server/api/endpoints/admin/unset-user-mutual-banner.ts diff --git a/locales/index.d.ts b/locales/index.d.ts index 9b676dc0c4..3a1dd92911 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2826,6 +2826,14 @@ export interface Locale extends ILocale { * バナーを解除しますか? */ "unsetUserBannerConfirm": string; + /** + * 相互バナーを解除 + */ + "unsetUserMutualBanner": string; + /** + * 相互バナーを解除しますか? + */ + "unsetUserMutualBannerConfirm": string; /** * すべてのファイルを削除 */ @@ -5683,6 +5691,18 @@ export interface Locale extends ILocale { "autoSuspendedForNotResponding": string; }; }; + /** + * 相互バナー + */ + "mutualBanner": string; + /** + * このユーザーのバナー + */ + "mutualBannerThisUser": string; + /** + * 最大 + */ + "maximum": string; "_bubbleGame": { /** * 遊び方 @@ -9415,6 +9435,10 @@ export interface Locale extends ILocale { * ユーザーのバーナーを削除する */ "write:admin:unset-user-banner": string; + /** + * ユーザーの相互バナーを削除する + */ + "write:admin:unset-user-mutual-banner": string; /** * ユーザーの凍結を解除する */ @@ -10017,6 +10041,22 @@ export interface Locale extends ILocale { * 最大{max}つまでデコレーションを付けられます。 */ "avatarDecorationMax": ParameterizedString<"max">; + /** + * 自身の相互リンクのバナーを設定 + */ + "myMutualBanner": string; + /** + * あなた自身が相互リンクのバナーとして設定してほしい画像を設定することができます。 + */ + "myMutualBannerDescription": string; + /** + * 相互リンクのバナー + */ + "mutualBanner": string; + /** + * 説明 + */ + "mutualBannerDescriptionEdit": string; }; "_exportOrImport": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 851518e49b..64df68bc7c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -701,6 +701,8 @@ unsetUserAvatar: "アイコンを解除" unsetUserAvatarConfirm: "アイコンを解除しますか?" unsetUserBanner: "バナーを解除" unsetUserBannerConfirm: "バナーを解除しますか?" +unsetUserMutualBanner: "相互バナーを解除" +unsetUserMutualBannerConfirm: "相互バナーを解除しますか?" deleteAllFiles: "すべてのファイルを削除" deleteAllFilesConfirm: "すべてのファイルを削除しますか?" removeAllFollowing: "フォローを全解除" @@ -1425,6 +1427,9 @@ _delivery: manuallySuspended: "手動停止中" goneSuspended: "サーバー削除のため停止中" autoSuspendedForNotResponding: "サーバー応答なしのため停止中" +mutualBanner: "相互バナー" +mutualBannerThisUser: "このユーザーのバナー" +maximum: "最大" _bubbleGame: howToPlay: "遊び方" @@ -2476,6 +2481,7 @@ _permissions: "write:admin:suspend-user": "ユーザーを凍結する" "write:admin:unset-user-avatar": "ユーザーのアバターを削除する" "write:admin:unset-user-banner": "ユーザーのバーナーを削除する" + "write:admin:unset-user-mutual-banner": "ユーザーの相互バナーを削除する" "write:admin:unsuspend-user": "ユーザーの凍結を解除する" "write:admin:meta": "インスタンスのメタデータを操作する" "write:admin:user-note": "モデレーションノートを操作する" @@ -2641,6 +2647,10 @@ _profile: changeBanner: "バナー画像を変更" verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。" + myMutualBanner: "自身の相互リンクのバナーを設定" + myMutualBannerDescription: "あなた自身が相互リンクのバナーとして設定してほしい画像を設定することができます。" + mutualBanner: "相互リンクのバナー" + mutualBannerDescriptionEdit: "説明" _exportOrImport: allNotes: "全てのノート" diff --git a/packages/backend/migration/1723213482131-mutualBanner.js b/packages/backend/migration/1723213482131-mutualBanner.js new file mode 100644 index 0000000000..c3bcb34ce3 --- /dev/null +++ b/packages/backend/migration/1723213482131-mutualBanner.js @@ -0,0 +1,27 @@ +export class MutualBanner1723213482131 { + name = 'MutualBanner1723213482131' + + async up(queryRunner) { + await queryRunner.query(`CREATE TABLE "user_banner" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "description" character varying(1024), "url" character varying(1024), "fileId" character varying(32) NOT NULL, CONSTRAINT "PK_0d9a418f048e308dbfb6562149d" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_fa06ea2e2375449537ced781f1" ON "user_banner" ("userId") `); + await queryRunner.query(`CREATE TABLE "user_banner_pining" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "pinnedBannerId" character varying(32) NOT NULL, CONSTRAINT "PK_970d24f72e8d2b20f8c21ec5d11" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_3b74dc21b68da606011c81609c" ON "user_banner_pining" ("userId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_7d51b5a8ae859e0023a98837a1" ON "user_banner_pining" ("userId", "pinnedBannerId") `); + await queryRunner.query(`ALTER TABLE "user_banner" ADD CONSTRAINT "FK_fa06ea2e2375449537ced781f15" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_banner" ADD CONSTRAINT "FK_3de9f17cce2c10f6938fb261c0b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_banner_pining" ADD CONSTRAINT "FK_3b74dc21b68da606011c81609c9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_banner_pining" ADD CONSTRAINT "FK_d13be8242980f7018d664f780f6" FOREIGN KEY ("pinnedBannerId") REFERENCES "user_banner"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_banner_pining" DROP CONSTRAINT "FK_d13be8242980f7018d664f780f6"`); + await queryRunner.query(`ALTER TABLE "user_banner_pining" DROP CONSTRAINT "FK_3b74dc21b68da606011c81609c9"`); + await queryRunner.query(`ALTER TABLE "user_banner" DROP CONSTRAINT "FK_3de9f17cce2c10f6938fb261c0b"`); + await queryRunner.query(`ALTER TABLE "user_banner" DROP CONSTRAINT "FK_fa06ea2e2375449537ced781f15"`); + await queryRunner.query(`DROP INDEX "public"."IDX_7d51b5a8ae859e0023a98837a1"`); + await queryRunner.query(`DROP INDEX "public"."IDX_3b74dc21b68da606011c81609c"`); + await queryRunner.query(`DROP TABLE "user_banner_pining"`); + await queryRunner.query(`DROP INDEX "public"."IDX_fa06ea2e2375449537ced781f1"`); + await queryRunner.query(`DROP TABLE "user_banner"`); + } +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 8b1a19153d..f3b7749377 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -13,6 +13,8 @@ import { import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { UserSearchService } from '@/core/UserSearchService.js'; +import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js'; +import { UserBannerPiningEntityService } from '@/core/entities/UserBannerPiningEntityService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AiService } from './AiService.js'; @@ -45,6 +47,8 @@ import { NoteCreateService } from './NoteCreateService.js'; import { NoteUpdateService } from './NoteUpdateService.js'; import { NoteDeleteService } from './NoteDeleteService.js'; import { NotePiningService } from './NotePiningService.js'; +import { UserBannerPiningService } from './UserBannerPiningService.js'; +import { UserBannerService } from './UserBannerService.js'; import { NoteReadService } from './NoteReadService.js'; import { NotificationService } from './NotificationService.js'; import { PollService } from './PollService.js'; @@ -196,6 +200,8 @@ const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting const $NoteUpdateService: Provider = { provide: 'NoteUpdateService', useExisting: NoteUpdateService }; const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService }; const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService }; +const $UserBannerPiningService: Provider = { provide: 'UserBannerPiningService', useExisting: UserBannerPiningService }; +const $UserBannerService: Provider = { provide: 'UserBannerService', useExisting: UserBannerService }; const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService }; const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService }; const $PollService: Provider = { provide: 'PollService', useExisting: PollService }; @@ -283,6 +289,8 @@ const $UserEntityService: Provider = { provide: 'UserEntityService', useExisting const $UserGroupEntityService: Provider = { provide: 'UserGroupEntityService', useExisting: UserGroupEntityService }; const $UserGroupInvitationEntityService: Provider = { provide: 'UserGroupInvitationEntityService', useExisting: UserGroupInvitationEntityService }; const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService }; +const $UserBannerEntityService: Provider = { provide: 'UserBannerEntityService', useExisting: UserBannerEntityService }; +const $UserBannerPiningEntityService: Provider = { provide: 'UserBannerPiningEntityService', useExisting: UserBannerPiningEntityService }; const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; @@ -353,6 +361,8 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame NoteUpdateService, NoteDeleteService, NotePiningService, + UserBannerPiningService, + UserBannerService, NoteReadService, NotificationService, PollService, @@ -440,6 +450,8 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame UserGroupEntityService, UserGroupInvitationEntityService, UserListEntityService, + UserBannerEntityService, + UserBannerPiningEntityService, FlashEntityService, FlashLikeEntityService, RoleEntityService, @@ -506,6 +518,8 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame $NoteUpdateService, $NoteDeleteService, $NotePiningService, + $UserBannerService, + $UserBannerPiningService, $NoteReadService, $NotificationService, $PollService, @@ -593,6 +607,8 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame $UserGroupEntityService, $UserGroupInvitationEntityService, $UserListEntityService, + $UserBannerEntityService, + $UserBannerPiningEntityService, $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, @@ -660,6 +676,8 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame NoteUpdateService, NoteDeleteService, NotePiningService, + UserBannerService, + UserBannerPiningService, NoteReadService, NotificationService, PollService, @@ -746,6 +764,8 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame UserGroupEntityService, UserGroupInvitationEntityService, UserListEntityService, + UserBannerEntityService, + UserBannerPiningEntityService, FlashEntityService, FlashLikeEntityService, RoleEntityService, @@ -812,6 +832,8 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame $NoteUpdateService, $NoteDeleteService, $NotePiningService, + $UserBannerService, + $UserBannerPiningService, $NoteReadService, $NotificationService, $PollService, @@ -898,6 +920,8 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame $UserGroupEntityService, $UserGroupInvitationEntityService, $UserListEntityService, + $UserBannerEntityService, + $UserBannerPiningEntityService, $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, diff --git a/packages/backend/src/core/UserBannerPiningService.ts b/packages/backend/src/core/UserBannerPiningService.ts new file mode 100644 index 0000000000..1e98876786 --- /dev/null +++ b/packages/backend/src/core/UserBannerPiningService.ts @@ -0,0 +1,55 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { In } from 'typeorm'; +import { bindThis } from '@/decorators.js'; +import { MiUser } from '@/models/User.js'; +import { MiUserBanner } from '@/models/UserBanner.js'; +import type { MiUserBannerPining, UserBannerPiningRepository, UserBannerRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { IdService } from '@/core/IdService.js'; + +@Injectable() +export class UserBannerPiningService { + constructor( + @Inject(DI.userBannerRepository) + private userBannerRepository: UserBannerRepository, + @Inject(DI.userBannerPiningRepository) + private userBannerPiningRepository: UserBannerPiningRepository, + + private idService: IdService, + ) { + + } + + /** + * 指定したユーザーのバナーをピン留めします + * @param userId + * @param bannerIds + */ + public async addPinned(userId: MiUser['id'], bannerIds: MiUserBanner['id'][]) { + const pinsToInsert = bannerIds.map(bannerId => ({ + id: this.idService.gen(), + userId, + pinnedBannerId: bannerId, + } as MiUserBannerPining)); + await this.userBannerPiningRepository + .createQueryBuilder() + .insert() + .values(pinsToInsert) + .orIgnore() + .execute(); + } + + /** + * 指定したユーザーのバナーのピン留めを解除します + * @param userId + * @param bannerIds + */ + @bindThis + public async removePinned(userId:MiUser['id'], bannerIds:MiUserBanner['id'][]) { + await this.userBannerPiningRepository.delete({ + userId, + pinnedBannerId: In(bannerIds), + }); + } +} diff --git a/packages/backend/src/core/UserBannerService.ts b/packages/backend/src/core/UserBannerService.ts new file mode 100644 index 0000000000..ff381988f2 --- /dev/null +++ b/packages/backend/src/core/UserBannerService.ts @@ -0,0 +1,115 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; +import { MiUser } from '@/models/User.js'; +import { MiUserBanner } from '@/models/UserBanner.js'; +import type { DriveFilesRepository, MiDriveFile, UserBannerRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { IdService } from '@/core/IdService.js'; + +@Injectable() +export class UserBannerService { + constructor( + @Inject(DI.userBannerRepository) + private userBannerRepository: UserBannerRepository, + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + + private idService: IdService, + ) { + + } + + /** + * 指定したユーザーのバナーを作成します + * @param userId + * @param description + * @param url + * @param fileId + */ + @bindThis + public async create(userId: MiUser['id'], description: string | null, url: string, fileId: MiDriveFile['id']) { + const banner = await this.userBannerRepository.findOneBy({ + userId, + }); + + if (banner) throw new IdentifiableError('9dab45d9-cc66-4dfa-8305-610834e7f256', 'Already exists.'); + + const file = await this.driveFilesRepository.findOneBy({ + id: fileId, + }); + + if (file == null) throw new IdentifiableError('e61187d1-9270-426b-8dc6-6b233c545133', 'No such file.'); + + return await this.userBannerRepository.insert({ + id: this.idService.gen(), + userId, + description: description ?? null, + fileId: file.id, + url: url, + } as MiUserBanner); + } + + /** + * 指定したユーザーのバナーを更新します + * @param userId + * @param bannerId + * @param description + * @param url + * @param fileId + */ + @bindThis + public async update(userId: MiUser['id'], bannerId: MiUserBanner['id'], description: string | null, url: string | null, fileId: MiDriveFile['id'] ) { + const banner = await this.userBannerRepository.findOneBy({ + id: bannerId, + }); + + if (banner == null) { + throw new IdentifiableError('ac26da32-1659-4fbb-82c2-fc11a494799f', 'No such banner.'); + } + + if (banner.userId !== userId) { + throw new IdentifiableError('dfe79730-96f7-4d65-8c2a-b0975bf3524c', 'Not this user banner.'); + } + + const file = await this.driveFilesRepository.findOneBy({ + id: fileId, + }); + + if (file == null) { + throw new IdentifiableError('e61187d1-9270-426b-8dc6-6b233c545133', 'No such file.'); + } + + await this.userBannerRepository.update({ + id: bannerId, + }, { + description: description ?? null, + fileId: file.id, + url: url ?? null, + }); + } + + /** + * 指定したユーザーのバナー削除します + * @param userId + * @param bannerId + */ + @bindThis + public async delete(userId: MiUser['id'], bannerId: MiUserBanner['id']) { + const banner = await this.userBannerRepository.findOneBy({ + id: bannerId, + }); + + if (banner == null) { + throw new IdentifiableError('f4b158a5-610f-4ed3-b228-3507ebe1bba6', 'No such banner.'); + } + + if (banner.userId !== userId) { + throw new IdentifiableError('ad84053d-0cf4-4446-ac72-209adef15835', 'Not this user banner.'); + } + + await this.userBannerRepository.delete({ + id: bannerId, + }); + } +} diff --git a/packages/backend/src/core/entities/UserBannerEntityService.ts b/packages/backend/src/core/entities/UserBannerEntityService.ts new file mode 100644 index 0000000000..25f4bb27bc --- /dev/null +++ b/packages/backend/src/core/entities/UserBannerEntityService.ts @@ -0,0 +1,56 @@ +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { bindThis } from '@/decorators.js'; +import type { MiUser } from '@/models/User.js'; +import { DI } from '@/di-symbols.js'; +import type { DriveFilesRepository, MiUserBanner, UserBannerRepository } from '@/models/_.js'; +import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import type { Packed } from '@/misc/json-schema.js'; + +@Injectable() +export class UserBannerEntityService implements OnModuleInit { + private userEntityService: UserEntityService; + constructor( + @Inject(DI.userBannerRepository) + private userBannerRepository: UserBannerRepository, + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + private moduleRef: ModuleRef, + ) { + } + + async onModuleInit() { + this.userEntityService = this.moduleRef.get(UserEntityService.name); + } + + @bindThis + public async pack( + src: MiUserBanner | MiUserBanner['id'] | null | undefined, + me: { id: MiUser['id'] } | null | undefined, + ): Promise> { + if (!src) throw new IdentifiableError('9dab45d9-cc66-4dfa-8305-610834e7f256', 'No such banner.'); + + const banner = typeof src === 'object' ? src : await this.userBannerRepository.findOneByOrFail({ id: src }); + const file = await this.driveFilesRepository.findOneByOrFail({ id: banner.fileId }); + + return { + id: banner.id, + user: await this.userEntityService.pack(banner.userId, me), + description: banner.description, + imgUrl: file.url, + url: banner.url, + fileId: file.id, + }; + } + + @bindThis + public async packMany( + src: MiUserBanner[] | MiUserBanner['id'][], + me: { id: MiUser['id'] } | null | undefined, + ): Promise[]> { + return (await Promise.allSettled(src.map(x => this.pack(x, me)))) + .filter(result => result.status === 'fulfilled') + .map(result => (result as PromiseFulfilledResult>).value); + } +} diff --git a/packages/backend/src/core/entities/UserBannerPiningEntityService.ts b/packages/backend/src/core/entities/UserBannerPiningEntityService.ts new file mode 100644 index 0000000000..b550875c91 --- /dev/null +++ b/packages/backend/src/core/entities/UserBannerPiningEntityService.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { bindThis } from '@/decorators.js'; +import type { MiUser } from '@/models/User.js'; +import type { MiUserBannerPining } from '@/models/_.js'; +import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js'; +import { Packed } from '@/misc/json-schema.js'; + +@Injectable() +export class UserBannerPiningEntityService { + constructor( + private userBannerEntityService: UserBannerEntityService, + ) {} + + @bindThis + public async packMany( + src: MiUserBannerPining[], + me: { id: MiUser['id'] } | null | undefined, + ) : Promise[]> { + return (await Promise.allSettled(src.map(pining => this.userBannerEntityService.pack(pining.pinnedBannerId, me)))) + .filter(result => result.status === 'fulfilled') + .map(result => (result as PromiseFulfilledResult>).value); + } +} diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 252f1a2dd6..42e44955ca 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -29,17 +29,21 @@ import type { FollowRequestsRepository, MessagingMessagesRepository, MiFollowing, + MiUserBanner, MiUserNotePining, MiUserProfile, MutingsRepository, NoteUnreadsRepository, RenoteMutingsRepository, UserGroupJoiningsRepository, + UserBannerRepository, + UserBannerPiningRepository, UserMemoRepository, UserNotePiningsRepository, UserProfilesRepository, UserSecurityKeysRepository, UsersRepository, + MiUserBannerPining, } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; @@ -49,9 +53,10 @@ import { IdService } from '@/core/IdService.js'; import type { AnnouncementService } from '@/core/AnnouncementService.js'; import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; +import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js'; +import { UserBannerPiningEntityService } from '@/core/entities/UserBannerPiningEntityService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { NoteEntityService } from './NoteEntityService.js'; -import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { PageEntityService } from './PageEntityService.js'; const Ajv = _Ajv.default; @@ -130,6 +135,11 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.userNotePiningsRepository) private userNotePiningsRepository: UserNotePiningsRepository, + @Inject(DI.userBannerRepository) + private userBannerRepository: UserBannerRepository, + @Inject(DI.userBannerPiningRepository) + private userBannerPiningRepository: UserBannerPiningRepository, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -141,6 +151,9 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.userMemosRepository) private userMemosRepository: UserMemoRepository, + + private userBannerEntityService: UserBannerEntityService, + private userBannerPiningEntityService: UserBannerPiningEntityService, ) { } @@ -479,6 +492,8 @@ export class UserEntityService implements OnModuleInit { } let pins: MiUserNotePining[] = []; + let myMutualBanner: MiUserBanner | null = null; + let mutualBanners: MiUserBannerPining[] = []; if (isDetailed) { if (opts.pinNotes) { pins = opts.pinNotes.get(user.id) ?? []; @@ -489,6 +504,12 @@ export class UserEntityService implements OnModuleInit { .orderBy('pin.id', 'DESC') .getMany(); } + if (user.id) { + [myMutualBanner, mutualBanners] = await Promise.all([ + this.userBannerRepository.findOneBy({ userId: user.id }), + this.userBannerPiningRepository.findBy({ userId: user.id }), + ]); + } } const followingCount = profile == null ? null : @@ -573,6 +594,8 @@ export class UserEntityService implements OnModuleInit { lang: profile!.lang, fields: profile!.fields, verifiedLinks: profile!.verifiedLinks, + mutualBanners: mutualBanners.length > 0 ? this.userBannerPiningEntityService.packMany(mutualBanners, me) : [], + myMutualBanner: myMutualBanner ? this.userBannerEntityService.pack(myMutualBanner, me) : null, followersCount: followersCount ?? '?', followingCount: followingCount ?? '?', notesCount: user.notesCount, diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index e9427d8e34..a5f457c1af 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -31,6 +31,8 @@ export const DI = { pollsRepository: Symbol('pollsRepository'), pollVotesRepository: Symbol('pollVotesRepository'), userProfilesRepository: Symbol('userProfilesRepository'), + userBannerRepository: Symbol('userBannerRepository'), + userBannerPiningRepository: Symbol('userBannerPiningRepository'), userKeypairsRepository: Symbol('userKeypairsRepository'), userPendingsRepository: Symbol('userPendingsRepository'), userSecurityKeysRepository: Symbol('userSecurityKeysRepository'), diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index 6c1b92ef16..bc03aadf90 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -11,6 +11,7 @@ import { packedUserDetailedSchema, packedUserLiteSchema, packedUserSchema, + packedUserBannerSchema, } from '@/models/json-schema/user.js'; import { packedNoteSchema } from '@/models/json-schema/note.js'; import { packedUserListSchema } from '@/models/json-schema/user-list.js'; @@ -70,6 +71,7 @@ export const refs = { MeDetailed: packedMeDetailedSchema, UserDetailed: packedUserDetailedSchema, User: packedUserSchema, + UserBanner: packedUserBannerSchema, UserList: packedUserListSchema, UserGroup: packedUserGroupSchema, diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index b40e5241c2..0925bd86de 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -84,6 +84,8 @@ import { MiUserSecurityKey, MiWebhook, MiOfficialTag, + MiUserBannerPining, + MiUserBanner, } from './_.js'; import type { Provider } from '@nestjs/common'; import type { DataSource } from 'typeorm'; @@ -228,7 +230,19 @@ const $userGroupInvitationsRepository: Provider = { const $userNotePiningsRepository: Provider = { provide: DI.userNotePiningsRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserNotePining).extend(miRepository as MiRepository), + useFactory: (db: DataSource) => db.getRepository(MiUserNotePining), + inject: [DI.db], +}; + +const $userBannerRepository: Provider = { + provide: DI.userBannerRepository, + useFactory: (db: DataSource) => db.getRepository(MiUserBanner), + inject: [DI.db], +}; + +const $userBannerPiningRepository: Provider = { + provide: DI.userBannerPiningRepository, + useFactory: (db: DataSource) => db.getRepository(MiUserBannerPining), inject: [DI.db], }; @@ -571,6 +585,8 @@ const $officialTagRepository: Provider = { $userGroupJoiningsRepository, $userGroupInvitationsRepository, $userNotePiningsRepository, + $userBannerPiningRepository, + $userBannerRepository, $userIpsRepository, $usedUsernamesRepository, $followingsRepository, @@ -649,6 +665,8 @@ const $officialTagRepository: Provider = { $userGroupJoiningsRepository, $userGroupInvitationsRepository, $userNotePiningsRepository, + $userBannerPiningRepository, + $userBannerRepository, $userIpsRepository, $usedUsernamesRepository, $followingsRepository, diff --git a/packages/backend/src/models/UserBanner.ts b/packages/backend/src/models/UserBanner.ts new file mode 100644 index 0000000000..108587b02e --- /dev/null +++ b/packages/backend/src/models/UserBanner.ts @@ -0,0 +1,42 @@ +import { Entity, Column, Index, JoinColumn, PrimaryColumn, ManyToOne } from 'typeorm'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; +import { MiDriveFile } from './DriveFile.js'; + +@Entity('user_banner') +export class MiUserBanner { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public description: string | null; + @Column('varchar', { + length: 1024, + nullable: true, + }) + public url: string | null; + + @Column({ + ...id(), + }) + public fileId: MiDriveFile['id']; + + @ManyToOne(type => MiDriveFile, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public file: MiDriveFile; +} diff --git a/packages/backend/src/models/UserBannerPining.ts b/packages/backend/src/models/UserBannerPining.ts new file mode 100644 index 0000000000..754de012e9 --- /dev/null +++ b/packages/backend/src/models/UserBannerPining.ts @@ -0,0 +1,32 @@ +import { Entity, Column, Index, JoinColumn, PrimaryColumn, ManyToOne, OneToOne } from 'typeorm'; +import { MiUserBanner } from '@/models/UserBanner.js'; +import { id } from './util/id.js'; +import { MiUser } from './User.js'; + +@Entity('user_banner_pining') +@Index(['userId', 'pinnedBannerId'], { unique: true }) +export class MiUserBannerPining { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column(id()) + public userId: MiUser['id']; + + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column({ + ...id(), + }) + public pinnedBannerId: MiUserBanner['id']; + + @ManyToOne(type => MiUserBanner, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public pinnedBanner: MiUserBanner; +} diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index dddd434c03..ad9cbc1df6 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -4,7 +4,7 @@ */ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; -import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js'; +import { followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js'; import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiPage } from './Page.js'; diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index f44fd5f4ec..7ff7dbb94f 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -72,6 +72,8 @@ import { MiUserListMembership } from '@/models/UserListMembership.js'; import { MiUserNotePining } from '@/models/UserNotePining.js'; import { MiUserPending } from '@/models/UserPending.js'; import { MiUserProfile } from '@/models/UserProfile.js'; +import { MiUserBanner } from '@/models/UserBanner.js'; +import { MiUserBannerPining } from '@/models/UserBannerPining.js'; import { MiUserPublickey } from '@/models/UserPublickey.js'; import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; import { MiUserMemo } from '@/models/UserMemo.js'; @@ -195,6 +197,8 @@ export { MiUserNotePining, MiUserPending, MiUserProfile, + MiUserBanner, + MiUserBannerPining, MiUserPublickey, MiUserSecurityKey, MiWebhook, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index b8efacd1a8..54dbb37e49 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -184,6 +184,10 @@ export const packedUserLiteSchema = { type: 'number', nullable: false, optional: false, }, + behavior: { + type: 'string', + nullable: false, optional: true, + }, }, }, }, @@ -428,6 +432,80 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: true, }, + mutualBanners: { + type: 'array', + nullable: true, optional: false, + items: { + type: 'object', + nullable: false, optional: false, + properties: { + id: { + type: 'string', + format: 'id', + nullable: false, optional: false, + }, + user: { + type: 'object', + nullable: false, optional: false, + ref: 'UserLite', + }, + description: { + type: 'string', + nullable: true, optional: false, + }, + imgUrl: { + type: 'string', + format: 'url', + nullable: false, optional: false, + }, + url: { + type: 'string', + format: 'url', + nullable: false, optional: false, + }, + fileId: { + type: 'string', + format: 'id', + nullable: false, optional: false, + }, + }, + }, + }, + myMutualBanner: { + type: 'object', + nullable: true, optional: false, + properties: { + id: { + type: 'string', + format: 'id', + nullable: false, optional: false, + }, + user: { + type: 'object', + nullable: false, optional: false, + ref: 'UserLite', + }, + description: { + type: 'string', + nullable: true, optional: false, + }, + imgUrl: { + type: 'string', + format: 'url', + nullable: false, optional: false, + }, + url: { + type: 'string', + format: 'url', + nullable: false, optional: false, + }, + fileId: { + type: 'string', + format: 'id', + nullable: false, optional: false, + }, + }, + }, //#endregion }, } as const; @@ -730,3 +808,37 @@ export const packedUserSchema = { }, ], } as const; + +export const packedUserBannerSchema = { + type: 'object', + properties: { + id: { + type: 'string', + nullable: false, optional: false, + format: 'id', + }, + user: { + type: 'object', + nullable: false, optional: false, + ref: 'UserLite', + }, + description: { + type: 'string', + nullable: true, optional: false, + }, + imgUrl: { + type: 'string', + format: 'url', + nullable: false, optional: false, + }, + url: { + type: 'string', + nullable: true, optional: false, + }, + fileId: { + type: 'string', + format: 'id', + nullable: false, optional: false, + }, + }, +} as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 22c637be26..400e559e83 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -85,6 +85,8 @@ import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserMemo } from '@/models/UserMemo.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; +import { MiUserBanner } from '@/models/UserBanner.js'; +import { MiUserBannerPining } from '@/models/UserBannerPining.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; @@ -210,6 +212,8 @@ export const entities = [ MiFlash, MiFlashLike, MiUserMemo, + MiUserBanner, + MiUserBannerPining, MiBubbleGameRecord, MiReversiGame, ...charts, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 8663bbf459..b3811fa1f6 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -36,6 +36,7 @@ import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-d import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; +import * as ep___admin_unsetUserMutualBanner from './endpoints/admin/unset-user-mutual-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -444,6 +445,7 @@ const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-de const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default }; const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default }; +const $admin_unsetUserMutualBanner: Provider = { provide: 'ep:admin/unset-user-mutual-banner', useClass: ep___admin_unsetUserMutualBanner.default }; const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; @@ -857,6 +859,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, $admin_unsetUserBanner, + $admin_unsetUserMutualBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, @@ -1263,6 +1266,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, $admin_unsetUserBanner, + $admin_unsetUserMutualBanner, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, diff --git a/packages/backend/src/server/api/endpoint-base.ts b/packages/backend/src/server/api/endpoint-base.ts index f4c361f571..1e910706a9 100644 --- a/packages/backend/src/server/api/endpoint-base.ts +++ b/packages/backend/src/server/api/endpoint-base.ts @@ -19,6 +19,17 @@ const ajv = new Ajv({ }); ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); +ajv.addFormat('url', { + type: 'string', + validate: (url: string) => { + try { + new URL(url); + return true; + } catch (e) { + return false; + } + }, +}); export type Response = Record | void; diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 612355b19f..7ae16d55ce 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -40,6 +40,7 @@ import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-d import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; +import * as ep___admin_unsetUserMutualBanner from './endpoints/admin/unset-user-mutual-banner.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -447,6 +448,7 @@ const eps = [ ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], ['admin/unset-user-avatar', ep___admin_unsetUserAvatar], ['admin/unset-user-banner', ep___admin_unsetUserBanner], + ['admin/unset-user-mutual-banner', ep___admin_unsetUserMutualBanner], ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles], ['admin/drive/cleanup', ep___admin_drive_cleanup], ['admin/drive/files', ep___admin_drive_files], diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-mutual-banner.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-mutual-banner.ts new file mode 100644 index 0000000000..139a5e0204 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/unset-user-mutual-banner.ts @@ -0,0 +1,58 @@ +import { Inject, Injectable } from '@nestjs/common'; +import type { UserBannerRepository, UsersRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:unset-user-mutual-banner', +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userBannerRepository) + private userBannerRepository: UserBannerRepository, + private moderationLogService: ModerationLogService, + ) { + super(meta, paramDef, async (ps, me) => { + const user = await this.usersRepository.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + const mutualBanner = await this.userBannerRepository.findOneBy({ userId: user.id }); + + if (mutualBanner == null) return; + + await this.userBannerRepository.delete({ + id: mutualBanner.id, + }); + + this.moderationLogService.log(me, 'unsetUserMutualBanner', { + userId: user.id, + userUsername: user.username, + userBannerDescription: mutualBanner.description, + userBannerUrl: mutualBanner.url, + fileId: mutualBanner.fileId, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 2fc90dedc2..9ec6d766fa 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -11,7 +11,14 @@ import { JSDOM } from 'jsdom'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import * as Acct from '@/misc/acct.js'; -import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; +import type { + UsersRepository, + DriveFilesRepository, + UserProfilesRepository, + PagesRepository, + UserBannerRepository, + UserBannerPiningRepository, +} from '@/models/_.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js'; import type { MiUserProfile } from '@/models/UserProfile.js'; @@ -34,6 +41,8 @@ import type { Config } from '@/config.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { notificationRecieveConfig } from '@/models/json-schema/user.js'; +import { UserBannerService } from '@/core/UserBannerService.js'; +import { UserBannerPiningService } from '@/core/UserBannerPiningService.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; @@ -56,6 +65,12 @@ export const meta = { id: '539f3a45-f215-4f81-a9a8-31293640207f', }, + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'e0f0d3c7-e704-4314-a0b5-04286d69a65c', + }, + noSuchBanner: { message: 'No such banner file.', code: 'NO_SUCH_BANNER', @@ -68,6 +83,12 @@ export const meta = { id: 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191', }, + fileNotAnImage: { + message: 'The specified file is not an image.', + code: 'FILE_NOT_AN_IMAGE', + id: '2851568b-5ad1-4031-bf0d-5320afebf3a9', + }, + bannerNotAnImage: { message: 'The file specified as a banner is not an image.', code: 'BANNER_NOT_AN_IMAGE', @@ -217,6 +238,24 @@ export const paramDef = { uniqueItems: true, items: { type: 'string' }, }, + mutualBannerPining: { + type: 'array', + nullable: true, + items: { + type: 'string', + format: 'misskey:id', + }, + }, + myMutualBanner: { + type: 'object', + nullable: true, + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + description: { type: 'string' }, + url: { type: 'string', nullable: true, format: 'url' }, + }, + required: ['fileId'], + }, }, } as const; @@ -235,10 +274,17 @@ export default class extends Endpoint { // eslint- @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + @Inject(DI.userBannerRepository) + private userBannerRepository: UserBannerRepository, + @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, + @Inject(DI.userBannerPiningRepository) + private userBannerPiningRepository: UserBannerPiningRepository, + private userEntityService: UserEntityService, + private userBannerService: UserBannerService, private driveFileEntityService: DriveFileEntityService, private globalEventService: GlobalEventService, private userFollowingService: UserFollowingService, @@ -250,6 +296,7 @@ export default class extends Endpoint { // eslint- private cacheService: CacheService, private httpRequestService: HttpRequestService, private avatarDecorationService: AvatarDecorationService, + private userBannerPiningService: UserBannerPiningService, ) { super(meta, paramDef, async (ps, _user, token, flashToken) => { const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser; @@ -353,6 +400,50 @@ export default class extends Endpoint { // eslint- updates.avatarBlurhash = null; } + if (ps.mutualBannerPining) { + const bannerPiningNow = await this.userBannerPiningRepository.findBy({ userId: user.id }); + + const bannerPiningNowIds = new Set(bannerPiningNow.map(b => b.pinnedBannerId)); + const mutualBannerPiningIds = new Set(ps.mutualBannerPining); + + const bannersToAdd = [...mutualBannerPiningIds].filter(bannerId => !bannerPiningNowIds.has(bannerId)); + const bannersToRemove = [...bannerPiningNowIds].filter(bannerId => !mutualBannerPiningIds.has(bannerId)); + + if (bannersToAdd.length > 0) { + await this.userBannerPiningService.addPinned(user.id, bannersToAdd); + } + + if (bannersToRemove.length > 0) { + await this.userBannerPiningService.removePinned(user.id, bannersToRemove); + } + } + + if (ps.myMutualBanner) { + const banner = await this.userBannerRepository.findOneBy({ + userId: user.id, + }); + const file = await this.driveFilesRepository.findOneBy({ id: ps.myMutualBanner.fileId }); + const profileUrl = this.config.url + '/@' + user.username; + + if (file === null) throw new ApiError(meta.errors.noSuchFile); + if (!file.type.startsWith('image/')) throw new ApiError(meta.errors.fileNotAnImage); + + if (banner) { + await this.userBannerService.update(user.id, banner.id, ps.myMutualBanner.description ?? null, ps.myMutualBanner.url ?? profileUrl, ps.myMutualBanner.fileId); + } else { + await this.userBannerService.create(user.id, ps.myMutualBanner.description ?? null, ps.myMutualBanner.url ?? profileUrl, ps.myMutualBanner.fileId); + } + } + + if (ps.myMutualBanner === null) { + const banner = await this.userBannerRepository.findOneBy({ + userId: user.id, + }); + if (banner) { + await this.userBannerService.delete(user.id, banner.id); + } + } + if (ps.bannerId) { policies ??= await this.roleService.getUserPolicies(user.id); if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 78513d4e7c..1c3838bc9e 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -99,6 +99,7 @@ export const moderationLogTypes = [ 'updateAbuseReportNotificationRecipient', 'deleteAbuseReportNotificationRecipient', 'updateOfficialTags', + 'unsetUserMutualBanner', ] as const; export type ModerationLogPayloads = { @@ -326,6 +327,13 @@ export type ModerationLogPayloads = { priority: number, }[]; }; + unsetUserMutualBanner: { + userId: string; + userUsername: string; + userBannerDescription: string | null; + userBannerUrl: string | null; + fileId: string; + } }; export type Serialized = { diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index b0598fd578..6e2630f3a6 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -73,6 +73,8 @@ describe('ユーザー', () => { lang: user.lang, fields: user.fields, verifiedLinks: user.verifiedLinks, + myMutualBanner: user.myMutualBanner, + mutualBanners: user.mutualBanners, followersCount: user.followersCount, followingCount: user.followingCount, notesCount: user.notesCount, diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index 1c133e75c0..08d7b71364 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -400,6 +400,9 @@ type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requ // @public (undocumented) type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json']; +// @public (undocumented) +type AdminUnsetUserMutualBannerRequest = operations['admin___unset-user-mutual-banner']['requestBody']['content']['application/json']; + // @public (undocumented) type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json']; @@ -1233,6 +1236,7 @@ declare namespace entities { AdminDeleteAllFilesOfAUserRequest, AdminUnsetUserAvatarRequest, AdminUnsetUserBannerRequest, + AdminUnsetUserMutualBannerRequest, AdminDriveFilesRequest, AdminDriveFilesResponse, AdminDriveShowFileRequest, @@ -1799,6 +1803,7 @@ declare namespace entities { MeDetailed, UserDetailed, User, + UserBanner, UserList, UserGroup, Ad, @@ -2552,7 +2557,7 @@ type ModerationLog = { }); // @public (undocumented) -export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient"]; +export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "unsetUserMutualBanner"]; // @public (undocumented) type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json']; @@ -2834,7 +2839,7 @@ type PartialRolePolicyOverride = Partial<{ }>; // @public (undocumented) -export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:admin:official-tags", "write:admin:reindex"]; +export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unset-user-mutual-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:admin:official-tags", "write:admin:reindex"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; @@ -3136,6 +3141,9 @@ function toString_2(acct: Acct): string; // @public (undocumented) type User = components['schemas']['User']; +// @public (undocumented) +type UserBanner = components['schemas']['UserBanner']; + // @public (undocumented) type UserDetailed = components['schemas']['UserDetailed']; diff --git a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts index d03240f28d..082ddfa02d 100644 --- a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts +++ b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts @@ -331,6 +331,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-banner* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/cherrypick-js/src/autogen/endpoint.ts b/packages/cherrypick-js/src/autogen/endpoint.ts index 41475f2c4b..eb5a563b6f 100644 --- a/packages/cherrypick-js/src/autogen/endpoint.ts +++ b/packages/cherrypick-js/src/autogen/endpoint.ts @@ -44,6 +44,7 @@ import type { AdminDeleteAllFilesOfAUserRequest, AdminUnsetUserAvatarRequest, AdminUnsetUserBannerRequest, + AdminUnsetUserMutualBannerRequest, AdminDriveFilesRequest, AdminDriveFilesResponse, AdminDriveShowFileRequest, @@ -634,6 +635,7 @@ export type Endpoints = { 'admin/delete-all-files-of-a-user': { req: AdminDeleteAllFilesOfAUserRequest; res: EmptyResponse }; 'admin/unset-user-avatar': { req: AdminUnsetUserAvatarRequest; res: EmptyResponse }; 'admin/unset-user-banner': { req: AdminUnsetUserBannerRequest; res: EmptyResponse }; + 'admin/unset-user-mutual-banner': { req: AdminUnsetUserMutualBannerRequest; res: EmptyResponse }; 'admin/drive/clean-remote-files': { req: EmptyRequest; res: EmptyResponse }; 'admin/drive/cleanup': { req: EmptyRequest; res: EmptyResponse }; 'admin/drive/files': { req: AdminDriveFilesRequest; res: AdminDriveFilesResponse }; @@ -1041,6 +1043,7 @@ export const endpointReqTypes: Record - + @@ -27,7 +27,9 @@ const props = withDefaults(defineProps<{ url: string; rel?: null | string; navigationBehavior?: MkABehavior; + hideIcon?: boolean; }>(), { + hideIcon: false, }); const self = props.url.startsWith(local); diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue index aaebae985d..eff560d3d8 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -95,6 +95,9 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.resetPassword }} + {{ i18n.ts.unsetUserAvatar }} + {{ i18n.ts.unsetUserBanner }} + {{ i18n.ts.unsetUserMutualBanner }}
@@ -362,6 +365,18 @@ async function unsetUserBanner() { refreshUser(); } +async function unsetUserMutualBanner() { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.unsetUserMutualBannerConfirm, + }); + if (confirm.canceled) return; + + await os.apiWithDialog('admin/unset-user-mutual-banner', { + userId: user.value.id, + }).then(refreshUser); +} + async function deleteAllFiles() { const confirm = await os.confirm({ type: 'warning', diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 321e88ff33..8613f73263 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -87,6 +87,33 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + + + + + +
+

{{ i18n.ts._profile.mutualBanner }}

+ + {{ i18n.ts.selectFile }} +
+ + {{ i18n.ts.save }} + {{ i18n.ts.delete }} +
+ +
@@ -153,6 +180,12 @@ watch(() => profile, () => { }); const fields = ref($i.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []); +const myMutualBanner = ref<{ fileId: string; description?: string; url?: string | null; imgUrl?: string; }>({ + fileId: $i.myMutualBanner?.fileId ?? '', + description: $i.myMutualBanner?.description ?? '', + url: $i.myMutualBanner?.url ?? '', + imgUrl: $i.myMutualBanner?.imgUrl ?? '', +}); const fieldEditMode = ref(false); function addField() { @@ -178,6 +211,40 @@ function saveFields() { globalEvents.emit('requestClearPageCache'); } +function isValidUrl(url: string): boolean { + try { + new URL(url); + return true; + } catch (_) { + return false; + } +} + +function saveMyMutualBanner() { + if ( myMutualBanner.value.fileId === '' || myMutualBanner.value.url && !isValidUrl(myMutualBanner.value.url)) { + os.alert({ + type: 'error', + title: i18n.ts.invalidParamError, + text: i18n.ts.invalidParamErrorDescription, + }); + return; + } + os.apiWithDialog('i/update', { + myMutualBanner: { + fileId: myMutualBanner.value.fileId, + description: myMutualBanner.value.description, + url: myMutualBanner.value.url === '' ? null : myMutualBanner.value.url, + }, + }); +} + +function deleteMyMutualBanner() { + os.apiWithDialog('i/update', { + myMutualBanner: null, + }); + myMutualBanner.value = { fileId: '', description: '', url: '' }; +} + function save() { os.apiWithDialog('i/update', { // 空文字列をnullにしたいので??は使うな @@ -218,6 +285,13 @@ function save() { } } +function changeMutualBannerFile(ev: MouseEvent) { + selectFile(ev.currentTarget ?? ev.target, i18n.ts.mutualBanner).then(async (file) => { + myMutualBanner.value.imgUrl = file.url; + myMutualBanner.value.fileId = file.id; + }); +} + function changeAvatar(ev) { selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar).then(async (file) => { let originalOrCropped = file; @@ -376,4 +450,13 @@ definePageMetadata(() => ({ .dragItemForm { flex-grow: 1; } + +.mutualBannerImg { + max-width: 300px; + min-width: 200px; + max-height: 60px; + min-height: 40px; + object-fit: contain; +} + diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index e0178623cc..7ba87425c3 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -121,6 +121,27 @@ SPDX-License-Identifier: AGPL-3.0-only +
+ {{ i18n.ts.mutualBannerThisUser }} + +
+ + + + {{ (user.myMutualBanner?.description === '' || user.myMutualBanner?.description === null) ? i18n.ts.noDescription : user.myMutualBanner?.description }} + {{ i18n.ts.follow }} + {{ i18n.ts.unfollow }} +
+
+
+ {{ i18n.ts.mutualBanner }} +
+
+ + + +
+
@@ -196,6 +217,7 @@ import { miLocalStorage } from '@/local-storage.js'; import { editNickname } from '@/scripts/edit-nickname.js'; import { vibrate } from '@/scripts/vibrate.js'; import detectLanguage from '@/scripts/detect-language.js'; +import MkLink from '@/components/MkLink.vue'; function calcAge(birthdate: string): number { const date = new Date(birthdate); @@ -239,6 +261,7 @@ const editModerationNote = ref(false); const translation = ref(null); const translating = ref(false); +const mutualBanners = ref(props.user.mutualBanners); watch(moderationNote, async () => { await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); @@ -295,6 +318,23 @@ function showMemoTextarea() { }); } +function mutualBannerFollow(id: string) { + os.apiWithDialog('i/update', { + mutualBannerPining: [ + ...($i?.mutualBanners?.map(banner => banner.id) ?? []), + id, + ], + }); +} + +function mutualBannerUnFollow(id:string) { + os.apiWithDialog('i/update', { + mutualBannerPining: [ + ...($i?.mutualBanners?.map(banner => banner.id) ?? []).filter(bannerId => bannerId !== id), + ], + }); +} + function adjustMemoTextarea() { if (!memoTextareaEl.value) return; memoTextareaEl.value.style.height = '0px'; @@ -790,4 +830,28 @@ onUnmounted(() => { margin-left: 4px; color: var(--success); } + +.myMutualBanner { + display: flex; + justify-content: space-around; + align-items: center; + flex-flow: column wrap; + padding: 16px; +} + +.mutualBanner { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + padding: 16px; +} + +.mutualBannerImg { + max-width: 300px; + min-width: 200px; + max-height: 60px; + min-height: 40px; + object-fit: contain; +} + diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue index 46fa7b2768..a1b1118a6d 100644 --- a/packages/frontend/src/pages/user/index.timeline.vue +++ b/packages/frontend/src/pages/user/index.timeline.vue @@ -18,10 +18,25 @@ SPDX-License-Identifier: AGPL-3.0-only + - + +
+
+
+ + + +

{{ (mutualBanner.description === '' || mutualBanner.description === null) ? i18n.ts.noDescription : mutualBanner.description }}

+ {{ i18n.ts.unfollow }} +
+
+
+

{{ i18n.ts.nothing }}

+
+
@@ -32,12 +47,17 @@ import MkNotes from '@/components/MkNotes.vue'; import MkTab from '@/components/MkTab.vue'; import XReactions from '@/pages/user/reactions.vue'; import { i18n } from '@/i18n.js'; +import MkLink from '@/components/MkLink.vue'; +import MkButton from '@/components/MkButton.vue'; import { $i } from '@/account.js'; +import * as os from '@/os.js'; const props = defineProps<{ user: Misskey.entities.UserDetailed; }>(); +const mutualBanners = ref(props.user.mutualBanners); + const tab = ref(null); const pagination = computed(() => tab.value === 'featured' ? { @@ -57,6 +77,17 @@ const pagination = computed(() => tab.value === 'featured' ? { withFiles: tab.value === 'files', }, }); + +function mutualBannerUnFollow(id:string) { + os.apiWithDialog('i/update', { + mutualBannerPining: [ + ...($i?.mutualBanners?.map(banner => banner.id) ?? []).filter(bannerId => bannerId !== id), + ], + }); + if (mutualBanners.value) { + mutualBanners.value = mutualBanners.value.filter(banner => banner.id !== id); + } +} From 05d760b72ad3afd9bed6f575f607ca17c7dbc41b Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Wed, 14 Aug 2024 22:44:56 +0900 Subject: [PATCH 02/12] =?UTF-8?q?enhance(profile):=20=E7=9B=B8=E4=BA=92?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF=E6=A9=9F=E8=83=BD=E3=81=AE=E6=94=B9?= =?UTF-8?q?=E4=BF=AE=20(MisskeyIO#684)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 5a9d8a556440452bdf803c323b93098e7c716a54) # Conflicts: # locales/ja-JP.yml # packages/backend/src/core/CoreModule.ts # packages/backend/src/core/RoleService.ts # packages/backend/src/core/entities/UserEntityService.ts # packages/backend/src/models/RepositoryModule.ts # packages/backend/src/models/_.ts # packages/backend/src/models/json-schema/role.ts # packages/backend/src/types.ts # packages/backend/test/e2e/users.ts # packages/cherrypick-js/etc/cherrypick-js.api.md # packages/cherrypick-js/src/autogen/endpoint.ts # packages/cherrypick-js/src/autogen/entities.ts # packages/cherrypick-js/src/autogen/types.ts # packages/cherrypick-js/src/consts.ts # packages/frontend/src/const.ts # packages/frontend/src/pages/admin-user.vue # packages/frontend/src/pages/user/home.vue # packages/frontend/src/pages/user/index.timeline.vue Co-authored-by: まっちゃてぃー。 <56515516+mattyatea@users.noreply.github.com> --- locales/index.d.ts | 68 ++++-- locales/ja-JP.yml | 25 ++- .../migration/1723213482131-mutualBanner.js | 27 --- .../migration/1723311628855-mutuallinks.js | 11 + packages/backend/src/core/CoreModule.ts | 24 --- packages/backend/src/core/RoleService.ts | 6 + .../src/core/UserBannerPiningService.ts | 55 ----- .../backend/src/core/UserBannerService.ts | 115 ---------- .../core/entities/UserBannerEntityService.ts | 56 ----- .../entities/UserBannerPiningEntityService.ts | 23 -- .../src/core/entities/UserEntityService.ts | 25 +-- packages/backend/src/di-symbols.ts | 2 - packages/backend/src/misc/json-schema.ts | 2 - .../backend/src/models/RepositoryModule.ts | 18 -- packages/backend/src/models/UserBanner.ts | 42 ---- .../backend/src/models/UserBannerPining.ts | 32 --- packages/backend/src/models/UserProfile.ts | 13 ++ packages/backend/src/models/_.ts | 4 - .../backend/src/models/json-schema/role.ts | 8 + .../backend/src/models/json-schema/user.ts | 131 ++---------- packages/backend/src/postgres.ts | 4 - .../backend/src/server/api/EndpointsModule.ts | 8 +- packages/backend/src/server/api/endpoints.ts | 4 +- ...al-banner.ts => unset-user-mutual-link.ts} | 28 ++- .../src/server/api/endpoints/i/update.ts | 118 +++++----- packages/backend/src/types.ts | 8 +- packages/backend/test/e2e/users.ts | 3 +- .../cherrypick-js/etc/cherrypick-js.api.md | 12 +- .../src/autogen/apiClientJSDoc.ts | 4 +- .../cherrypick-js/src/autogen/endpoint.ts | 6 +- .../cherrypick-js/src/autogen/entities.ts | 2 +- packages/cherrypick-js/src/autogen/models.ts | 1 - packages/cherrypick-js/src/autogen/types.ts | 79 +++---- packages/cherrypick-js/src/consts.ts | 9 - packages/frontend/src/const.ts | 2 + packages/frontend/src/pages/admin-user.vue | 6 +- .../frontend/src/pages/admin/roles.editor.vue | 38 ++++ packages/frontend/src/pages/admin/roles.vue | 14 ++ .../frontend/src/pages/settings/profile.vue | 202 ++++++++++++------ packages/frontend/src/pages/user/home.vue | 87 +++----- .../src/pages/user/index.timeline.vue | 51 +---- 41 files changed, 469 insertions(+), 904 deletions(-) delete mode 100644 packages/backend/migration/1723213482131-mutualBanner.js create mode 100644 packages/backend/migration/1723311628855-mutuallinks.js delete mode 100644 packages/backend/src/core/UserBannerPiningService.ts delete mode 100644 packages/backend/src/core/UserBannerService.ts delete mode 100644 packages/backend/src/core/entities/UserBannerEntityService.ts delete mode 100644 packages/backend/src/core/entities/UserBannerPiningEntityService.ts delete mode 100644 packages/backend/src/models/UserBanner.ts delete mode 100644 packages/backend/src/models/UserBannerPining.ts rename packages/backend/src/server/api/endpoints/admin/{unset-user-mutual-banner.ts => unset-user-mutual-link.ts} (61%) diff --git a/locales/index.d.ts b/locales/index.d.ts index 3a1dd92911..683fff4f6c 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2827,13 +2827,13 @@ export interface Locale extends ILocale { */ "unsetUserBannerConfirm": string; /** - * 相互バナーを解除 + * 相互リンクを解除 */ - "unsetUserMutualBanner": string; + "unsetUserMutualLink": string; /** - * 相互バナーを解除しますか? + * 相互リンクを解除しますか? */ - "unsetUserMutualBannerConfirm": string; + "unsetUserMutualLinkConfirm": string; /** * すべてのファイルを削除 */ @@ -5692,9 +5692,9 @@ export interface Locale extends ILocale { }; }; /** - * 相互バナー + * 相互リンク */ - "mutualBanner": string; + "mutualLink": string; /** * このユーザーのバナー */ @@ -7752,6 +7752,14 @@ export interface Locale extends ILocale { * アイコンデコレーションの最大取付個数 */ "avatarDecorationLimit": string; + /** + * 相互リンクのセクションの最大数 + */ + "mutualLinkSectionLimit": string; + /** + * セクション内の相互リンクの最大数 + */ + "mutualLinkLimit": string; }; "_condition": { /** @@ -9436,9 +9444,9 @@ export interface Locale extends ILocale { */ "write:admin:unset-user-banner": string; /** - * ユーザーの相互バナーを削除する + * ユーザーの相互リンクを削除する */ - "write:admin:unset-user-mutual-banner": string; + "write:admin:unset-user-mutual-link": string; /** * ユーザーの凍結を解除する */ @@ -10042,21 +10050,49 @@ export interface Locale extends ILocale { */ "avatarDecorationMax": ParameterizedString<"max">; /** - * 自身の相互リンクのバナーを設定 - */ - "myMutualBanner": string; - /** - * あなた自身が相互リンクのバナーとして設定してほしい画像を設定することができます。 + * 相互リンクを編集 */ - "myMutualBannerDescription": string; + "mutualLinksEdit": string; /** * 相互リンクのバナー */ - "mutualBanner": string; + "mutualLinksBanner": string; /** * 説明 */ - "mutualBannerDescriptionEdit": string; + "mutualLinksDescriptionEdit": string; + /** + * リンク先のURL + */ + "mutualLinksUrl": string; + /** + * このセクションをプロフィールにピン留め + */ + "mutualLinkPining": string; + /** + * 相互リンクを設定すると、あなたのプロフィールにバナーが表示されます。 + */ + "mutualLinksDescription": string; + /** + * 相互リンクを追加 + */ + "addMutualLink": string; + /** + * セクションを追加 + */ + "addMutualLinkSection": string; + /** + * セクション名 + */ + "sectionName": string; + /** + * セクション名を表示しないようにする + */ + "sectionNameNoneDescription": string; + /** + * セクション名を表示しない + */ + "sectionNameNone": string; }; "_exportOrImport": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 64df68bc7c..05b634eef0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -701,8 +701,8 @@ unsetUserAvatar: "アイコンを解除" unsetUserAvatarConfirm: "アイコンを解除しますか?" unsetUserBanner: "バナーを解除" unsetUserBannerConfirm: "バナーを解除しますか?" -unsetUserMutualBanner: "相互バナーを解除" -unsetUserMutualBannerConfirm: "相互バナーを解除しますか?" +unsetUserMutualLink: "相互リンクを削除" +unsetUserMutualLinkConfirm: "相互リンクを削除しますか?" deleteAllFiles: "すべてのファイルを削除" deleteAllFilesConfirm: "すべてのファイルを削除しますか?" removeAllFollowing: "フォローを全解除" @@ -1427,7 +1427,7 @@ _delivery: manuallySuspended: "手動停止中" goneSuspended: "サーバー削除のため停止中" autoSuspendedForNotResponding: "サーバー応答なしのため停止中" -mutualBanner: "相互バナー" +mutualLink: "相互リンク" mutualBannerThisUser: "このユーザーのバナー" maximum: "最大" @@ -2012,6 +2012,8 @@ _role: canAdvancedSearchNotes: "高度な検索の利用" canUseTranslator: "翻訳機能の利用" avatarDecorationLimit: "アイコンデコレーションの最大取付個数" + mutualLinkSectionLimit: "相互リンクのセクションの最大数" + mutualLinkLimit: "セクション内の相互リンクの最大数" _condition: roleAssignedTo: "マニュアルロールにアサイン済み" isLocal: "ローカルユーザー" @@ -2481,7 +2483,7 @@ _permissions: "write:admin:suspend-user": "ユーザーを凍結する" "write:admin:unset-user-avatar": "ユーザーのアバターを削除する" "write:admin:unset-user-banner": "ユーザーのバーナーを削除する" - "write:admin:unset-user-mutual-banner": "ユーザーの相互バナーを削除する" + "write:admin:unset-user-mutual-link": "ユーザーの相互リンクを削除する" "write:admin:unsuspend-user": "ユーザーの凍結を解除する" "write:admin:meta": "インスタンスのメタデータを操作する" "write:admin:user-note": "モデレーションノートを操作する" @@ -2647,10 +2649,17 @@ _profile: changeBanner: "バナー画像を変更" verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。" avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。" - myMutualBanner: "自身の相互リンクのバナーを設定" - myMutualBannerDescription: "あなた自身が相互リンクのバナーとして設定してほしい画像を設定することができます。" - mutualBanner: "相互リンクのバナー" - mutualBannerDescriptionEdit: "説明" + mutualLinksEdit: "相互リンクを編集" + mutualLinksBanner: "相互リンクのバナー" + mutualLinksDescriptionEdit: "説明" + mutualLinksUrl: "リンク先のURL" + mutualLinkPining: "このセクションをプロフィールにピン留め" + mutualLinksDescription: "相互リンクを設定すると、あなたのプロフィールにバナーが表示されます。" + addMutualLink: "相互リンクを追加" + addMutualLinkSection: "セクションを追加" + sectionName: "セクション名" + sectionNameNoneDescription: "セクション名を表示しないようにする" + sectionNameNone: "セクション名を表示しない" _exportOrImport: allNotes: "全てのノート" diff --git a/packages/backend/migration/1723213482131-mutualBanner.js b/packages/backend/migration/1723213482131-mutualBanner.js deleted file mode 100644 index c3bcb34ce3..0000000000 --- a/packages/backend/migration/1723213482131-mutualBanner.js +++ /dev/null @@ -1,27 +0,0 @@ -export class MutualBanner1723213482131 { - name = 'MutualBanner1723213482131' - - async up(queryRunner) { - await queryRunner.query(`CREATE TABLE "user_banner" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "description" character varying(1024), "url" character varying(1024), "fileId" character varying(32) NOT NULL, CONSTRAINT "PK_0d9a418f048e308dbfb6562149d" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_fa06ea2e2375449537ced781f1" ON "user_banner" ("userId") `); - await queryRunner.query(`CREATE TABLE "user_banner_pining" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "pinnedBannerId" character varying(32) NOT NULL, CONSTRAINT "PK_970d24f72e8d2b20f8c21ec5d11" PRIMARY KEY ("id"))`); - await queryRunner.query(`CREATE INDEX "IDX_3b74dc21b68da606011c81609c" ON "user_banner_pining" ("userId") `); - await queryRunner.query(`CREATE UNIQUE INDEX "IDX_7d51b5a8ae859e0023a98837a1" ON "user_banner_pining" ("userId", "pinnedBannerId") `); - await queryRunner.query(`ALTER TABLE "user_banner" ADD CONSTRAINT "FK_fa06ea2e2375449537ced781f15" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_banner" ADD CONSTRAINT "FK_3de9f17cce2c10f6938fb261c0b" FOREIGN KEY ("fileId") REFERENCES "drive_file"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_banner_pining" ADD CONSTRAINT "FK_3b74dc21b68da606011c81609c9" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - await queryRunner.query(`ALTER TABLE "user_banner_pining" ADD CONSTRAINT "FK_d13be8242980f7018d664f780f6" FOREIGN KEY ("pinnedBannerId") REFERENCES "user_banner"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); - } - - async down(queryRunner) { - await queryRunner.query(`ALTER TABLE "user_banner_pining" DROP CONSTRAINT "FK_d13be8242980f7018d664f780f6"`); - await queryRunner.query(`ALTER TABLE "user_banner_pining" DROP CONSTRAINT "FK_3b74dc21b68da606011c81609c9"`); - await queryRunner.query(`ALTER TABLE "user_banner" DROP CONSTRAINT "FK_3de9f17cce2c10f6938fb261c0b"`); - await queryRunner.query(`ALTER TABLE "user_banner" DROP CONSTRAINT "FK_fa06ea2e2375449537ced781f15"`); - await queryRunner.query(`DROP INDEX "public"."IDX_7d51b5a8ae859e0023a98837a1"`); - await queryRunner.query(`DROP INDEX "public"."IDX_3b74dc21b68da606011c81609c"`); - await queryRunner.query(`DROP TABLE "user_banner_pining"`); - await queryRunner.query(`DROP INDEX "public"."IDX_fa06ea2e2375449537ced781f1"`); - await queryRunner.query(`DROP TABLE "user_banner"`); - } -} diff --git a/packages/backend/migration/1723311628855-mutuallinks.js b/packages/backend/migration/1723311628855-mutuallinks.js new file mode 100644 index 0000000000..917bd4ec37 --- /dev/null +++ b/packages/backend/migration/1723311628855-mutuallinks.js @@ -0,0 +1,11 @@ +export class Mutuallinks1723311628855 { + name = 'Mutuallinks1723311628855' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutualLinkSections" jsonb NOT NULL DEFAULT '[]'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutualLinkSections"`); + } +} diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index f3b7749377..8b1a19153d 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -13,8 +13,6 @@ import { import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { UserSearchService } from '@/core/UserSearchService.js'; -import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js'; -import { UserBannerPiningEntityService } from '@/core/entities/UserBannerPiningEntityService.js'; import { AccountMoveService } from './AccountMoveService.js'; import { AccountUpdateService } from './AccountUpdateService.js'; import { AiService } from './AiService.js'; @@ -47,8 +45,6 @@ import { NoteCreateService } from './NoteCreateService.js'; import { NoteUpdateService } from './NoteUpdateService.js'; import { NoteDeleteService } from './NoteDeleteService.js'; import { NotePiningService } from './NotePiningService.js'; -import { UserBannerPiningService } from './UserBannerPiningService.js'; -import { UserBannerService } from './UserBannerService.js'; import { NoteReadService } from './NoteReadService.js'; import { NotificationService } from './NotificationService.js'; import { PollService } from './PollService.js'; @@ -200,8 +196,6 @@ const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting const $NoteUpdateService: Provider = { provide: 'NoteUpdateService', useExisting: NoteUpdateService }; const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService }; const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService }; -const $UserBannerPiningService: Provider = { provide: 'UserBannerPiningService', useExisting: UserBannerPiningService }; -const $UserBannerService: Provider = { provide: 'UserBannerService', useExisting: UserBannerService }; const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService }; const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService }; const $PollService: Provider = { provide: 'PollService', useExisting: PollService }; @@ -289,8 +283,6 @@ const $UserEntityService: Provider = { provide: 'UserEntityService', useExisting const $UserGroupEntityService: Provider = { provide: 'UserGroupEntityService', useExisting: UserGroupEntityService }; const $UserGroupInvitationEntityService: Provider = { provide: 'UserGroupInvitationEntityService', useExisting: UserGroupInvitationEntityService }; const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService }; -const $UserBannerEntityService: Provider = { provide: 'UserBannerEntityService', useExisting: UserBannerEntityService }; -const $UserBannerPiningEntityService: Provider = { provide: 'UserBannerPiningEntityService', useExisting: UserBannerPiningEntityService }; const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; @@ -361,8 +353,6 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame NoteUpdateService, NoteDeleteService, NotePiningService, - UserBannerPiningService, - UserBannerService, NoteReadService, NotificationService, PollService, @@ -450,8 +440,6 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame UserGroupEntityService, UserGroupInvitationEntityService, UserListEntityService, - UserBannerEntityService, - UserBannerPiningEntityService, FlashEntityService, FlashLikeEntityService, RoleEntityService, @@ -518,8 +506,6 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame $NoteUpdateService, $NoteDeleteService, $NotePiningService, - $UserBannerService, - $UserBannerPiningService, $NoteReadService, $NotificationService, $PollService, @@ -607,8 +593,6 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame $UserGroupEntityService, $UserGroupInvitationEntityService, $UserListEntityService, - $UserBannerEntityService, - $UserBannerPiningEntityService, $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, @@ -676,8 +660,6 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame NoteUpdateService, NoteDeleteService, NotePiningService, - UserBannerService, - UserBannerPiningService, NoteReadService, NotificationService, PollService, @@ -764,8 +746,6 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame UserGroupEntityService, UserGroupInvitationEntityService, UserListEntityService, - UserBannerEntityService, - UserBannerPiningEntityService, FlashEntityService, FlashLikeEntityService, RoleEntityService, @@ -832,8 +812,6 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame $NoteUpdateService, $NoteDeleteService, $NotePiningService, - $UserBannerService, - $UserBannerPiningService, $NoteReadService, $NotificationService, $PollService, @@ -920,8 +898,6 @@ const $ApGameService: Provider = { provide: 'ApGameService', useExisting: ApGame $UserGroupEntityService, $UserGroupInvitationEntityService, $UserListEntityService, - $UserBannerEntityService, - $UserBannerPiningEntityService, $FlashEntityService, $FlashLikeEntityService, $RoleEntityService, diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 7860c868dc..d5cf3c5596 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -61,6 +61,8 @@ export type RolePolicies = { rateLimitFactor: number; avatarDecorationLimit: number; fileSizeLimit: number; + mutualLinkSectionLimit: number; + mutualLinkLimit: number; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -93,6 +95,8 @@ export const DEFAULT_POLICIES: RolePolicies = { rateLimitFactor: 1, avatarDecorationLimit: 1, fileSizeLimit: 50, + mutualLinkSectionLimit: 1, + mutualLinkLimit: 15, }; @Injectable() @@ -398,6 +402,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)), avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)), fileSizeLimit: calc('fileSizeLimit', vs => Math.max(...vs)), + mutualLinkSectionLimit: calc('mutualLinkSectionLimit', vs => Math.max(...vs)), + mutualLinkLimit: calc('mutualLinkLimit', vs => Math.max(...vs)), }; } diff --git a/packages/backend/src/core/UserBannerPiningService.ts b/packages/backend/src/core/UserBannerPiningService.ts deleted file mode 100644 index 1e98876786..0000000000 --- a/packages/backend/src/core/UserBannerPiningService.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { In } from 'typeorm'; -import { bindThis } from '@/decorators.js'; -import { MiUser } from '@/models/User.js'; -import { MiUserBanner } from '@/models/UserBanner.js'; -import type { MiUserBannerPining, UserBannerPiningRepository, UserBannerRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { IdService } from '@/core/IdService.js'; - -@Injectable() -export class UserBannerPiningService { - constructor( - @Inject(DI.userBannerRepository) - private userBannerRepository: UserBannerRepository, - @Inject(DI.userBannerPiningRepository) - private userBannerPiningRepository: UserBannerPiningRepository, - - private idService: IdService, - ) { - - } - - /** - * 指定したユーザーのバナーをピン留めします - * @param userId - * @param bannerIds - */ - public async addPinned(userId: MiUser['id'], bannerIds: MiUserBanner['id'][]) { - const pinsToInsert = bannerIds.map(bannerId => ({ - id: this.idService.gen(), - userId, - pinnedBannerId: bannerId, - } as MiUserBannerPining)); - await this.userBannerPiningRepository - .createQueryBuilder() - .insert() - .values(pinsToInsert) - .orIgnore() - .execute(); - } - - /** - * 指定したユーザーのバナーのピン留めを解除します - * @param userId - * @param bannerIds - */ - @bindThis - public async removePinned(userId:MiUser['id'], bannerIds:MiUserBanner['id'][]) { - await this.userBannerPiningRepository.delete({ - userId, - pinnedBannerId: In(bannerIds), - }); - } -} diff --git a/packages/backend/src/core/UserBannerService.ts b/packages/backend/src/core/UserBannerService.ts deleted file mode 100644 index ff381988f2..0000000000 --- a/packages/backend/src/core/UserBannerService.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { bindThis } from '@/decorators.js'; -import { MiUser } from '@/models/User.js'; -import { MiUserBanner } from '@/models/UserBanner.js'; -import type { DriveFilesRepository, MiDriveFile, UserBannerRepository } from '@/models/_.js'; -import { DI } from '@/di-symbols.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { IdService } from '@/core/IdService.js'; - -@Injectable() -export class UserBannerService { - constructor( - @Inject(DI.userBannerRepository) - private userBannerRepository: UserBannerRepository, - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - - private idService: IdService, - ) { - - } - - /** - * 指定したユーザーのバナーを作成します - * @param userId - * @param description - * @param url - * @param fileId - */ - @bindThis - public async create(userId: MiUser['id'], description: string | null, url: string, fileId: MiDriveFile['id']) { - const banner = await this.userBannerRepository.findOneBy({ - userId, - }); - - if (banner) throw new IdentifiableError('9dab45d9-cc66-4dfa-8305-610834e7f256', 'Already exists.'); - - const file = await this.driveFilesRepository.findOneBy({ - id: fileId, - }); - - if (file == null) throw new IdentifiableError('e61187d1-9270-426b-8dc6-6b233c545133', 'No such file.'); - - return await this.userBannerRepository.insert({ - id: this.idService.gen(), - userId, - description: description ?? null, - fileId: file.id, - url: url, - } as MiUserBanner); - } - - /** - * 指定したユーザーのバナーを更新します - * @param userId - * @param bannerId - * @param description - * @param url - * @param fileId - */ - @bindThis - public async update(userId: MiUser['id'], bannerId: MiUserBanner['id'], description: string | null, url: string | null, fileId: MiDriveFile['id'] ) { - const banner = await this.userBannerRepository.findOneBy({ - id: bannerId, - }); - - if (banner == null) { - throw new IdentifiableError('ac26da32-1659-4fbb-82c2-fc11a494799f', 'No such banner.'); - } - - if (banner.userId !== userId) { - throw new IdentifiableError('dfe79730-96f7-4d65-8c2a-b0975bf3524c', 'Not this user banner.'); - } - - const file = await this.driveFilesRepository.findOneBy({ - id: fileId, - }); - - if (file == null) { - throw new IdentifiableError('e61187d1-9270-426b-8dc6-6b233c545133', 'No such file.'); - } - - await this.userBannerRepository.update({ - id: bannerId, - }, { - description: description ?? null, - fileId: file.id, - url: url ?? null, - }); - } - - /** - * 指定したユーザーのバナー削除します - * @param userId - * @param bannerId - */ - @bindThis - public async delete(userId: MiUser['id'], bannerId: MiUserBanner['id']) { - const banner = await this.userBannerRepository.findOneBy({ - id: bannerId, - }); - - if (banner == null) { - throw new IdentifiableError('f4b158a5-610f-4ed3-b228-3507ebe1bba6', 'No such banner.'); - } - - if (banner.userId !== userId) { - throw new IdentifiableError('ad84053d-0cf4-4446-ac72-209adef15835', 'Not this user banner.'); - } - - await this.userBannerRepository.delete({ - id: bannerId, - }); - } -} diff --git a/packages/backend/src/core/entities/UserBannerEntityService.ts b/packages/backend/src/core/entities/UserBannerEntityService.ts deleted file mode 100644 index 25f4bb27bc..0000000000 --- a/packages/backend/src/core/entities/UserBannerEntityService.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; -import { bindThis } from '@/decorators.js'; -import type { MiUser } from '@/models/User.js'; -import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, MiUserBanner, UserBannerRepository } from '@/models/_.js'; -import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import type { Packed } from '@/misc/json-schema.js'; - -@Injectable() -export class UserBannerEntityService implements OnModuleInit { - private userEntityService: UserEntityService; - constructor( - @Inject(DI.userBannerRepository) - private userBannerRepository: UserBannerRepository, - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - private moduleRef: ModuleRef, - ) { - } - - async onModuleInit() { - this.userEntityService = this.moduleRef.get(UserEntityService.name); - } - - @bindThis - public async pack( - src: MiUserBanner | MiUserBanner['id'] | null | undefined, - me: { id: MiUser['id'] } | null | undefined, - ): Promise> { - if (!src) throw new IdentifiableError('9dab45d9-cc66-4dfa-8305-610834e7f256', 'No such banner.'); - - const banner = typeof src === 'object' ? src : await this.userBannerRepository.findOneByOrFail({ id: src }); - const file = await this.driveFilesRepository.findOneByOrFail({ id: banner.fileId }); - - return { - id: banner.id, - user: await this.userEntityService.pack(banner.userId, me), - description: banner.description, - imgUrl: file.url, - url: banner.url, - fileId: file.id, - }; - } - - @bindThis - public async packMany( - src: MiUserBanner[] | MiUserBanner['id'][], - me: { id: MiUser['id'] } | null | undefined, - ): Promise[]> { - return (await Promise.allSettled(src.map(x => this.pack(x, me)))) - .filter(result => result.status === 'fulfilled') - .map(result => (result as PromiseFulfilledResult>).value); - } -} diff --git a/packages/backend/src/core/entities/UserBannerPiningEntityService.ts b/packages/backend/src/core/entities/UserBannerPiningEntityService.ts deleted file mode 100644 index b550875c91..0000000000 --- a/packages/backend/src/core/entities/UserBannerPiningEntityService.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { bindThis } from '@/decorators.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiUserBannerPining } from '@/models/_.js'; -import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js'; -import { Packed } from '@/misc/json-schema.js'; - -@Injectable() -export class UserBannerPiningEntityService { - constructor( - private userBannerEntityService: UserBannerEntityService, - ) {} - - @bindThis - public async packMany( - src: MiUserBannerPining[], - me: { id: MiUser['id'] } | null | undefined, - ) : Promise[]> { - return (await Promise.allSettled(src.map(pining => this.userBannerEntityService.pack(pining.pinnedBannerId, me)))) - .filter(result => result.status === 'fulfilled') - .map(result => (result as PromiseFulfilledResult>).value); - } -} diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 42e44955ca..3664632b04 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -29,21 +29,17 @@ import type { FollowRequestsRepository, MessagingMessagesRepository, MiFollowing, - MiUserBanner, MiUserNotePining, MiUserProfile, MutingsRepository, NoteUnreadsRepository, RenoteMutingsRepository, UserGroupJoiningsRepository, - UserBannerRepository, - UserBannerPiningRepository, UserMemoRepository, UserNotePiningsRepository, UserProfilesRepository, UserSecurityKeysRepository, UsersRepository, - MiUserBannerPining, } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; @@ -53,8 +49,6 @@ import { IdService } from '@/core/IdService.js'; import type { AnnouncementService } from '@/core/AnnouncementService.js'; import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; -import { UserBannerEntityService } from '@/core/entities/UserBannerEntityService.js'; -import { UserBannerPiningEntityService } from '@/core/entities/UserBannerPiningEntityService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { NoteEntityService } from './NoteEntityService.js'; import type { PageEntityService } from './PageEntityService.js'; @@ -135,11 +129,6 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.userNotePiningsRepository) private userNotePiningsRepository: UserNotePiningsRepository, - @Inject(DI.userBannerRepository) - private userBannerRepository: UserBannerRepository, - @Inject(DI.userBannerPiningRepository) - private userBannerPiningRepository: UserBannerPiningRepository, - @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, @@ -151,9 +140,6 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.userMemosRepository) private userMemosRepository: UserMemoRepository, - - private userBannerEntityService: UserBannerEntityService, - private userBannerPiningEntityService: UserBannerPiningEntityService, ) { } @@ -492,8 +478,6 @@ export class UserEntityService implements OnModuleInit { } let pins: MiUserNotePining[] = []; - let myMutualBanner: MiUserBanner | null = null; - let mutualBanners: MiUserBannerPining[] = []; if (isDetailed) { if (opts.pinNotes) { pins = opts.pinNotes.get(user.id) ?? []; @@ -504,12 +488,6 @@ export class UserEntityService implements OnModuleInit { .orderBy('pin.id', 'DESC') .getMany(); } - if (user.id) { - [myMutualBanner, mutualBanners] = await Promise.all([ - this.userBannerRepository.findOneBy({ userId: user.id }), - this.userBannerPiningRepository.findBy({ userId: user.id }), - ]); - } } const followingCount = profile == null ? null : @@ -594,8 +572,7 @@ export class UserEntityService implements OnModuleInit { lang: profile!.lang, fields: profile!.fields, verifiedLinks: profile!.verifiedLinks, - mutualBanners: mutualBanners.length > 0 ? this.userBannerPiningEntityService.packMany(mutualBanners, me) : [], - myMutualBanner: myMutualBanner ? this.userBannerEntityService.pack(myMutualBanner, me) : null, + mutualLinkSections: profile!.mutualLinkSections, followersCount: followersCount ?? '?', followingCount: followingCount ?? '?', notesCount: user.notesCount, diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index a5f457c1af..e9427d8e34 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -31,8 +31,6 @@ export const DI = { pollsRepository: Symbol('pollsRepository'), pollVotesRepository: Symbol('pollVotesRepository'), userProfilesRepository: Symbol('userProfilesRepository'), - userBannerRepository: Symbol('userBannerRepository'), - userBannerPiningRepository: Symbol('userBannerPiningRepository'), userKeypairsRepository: Symbol('userKeypairsRepository'), userPendingsRepository: Symbol('userPendingsRepository'), userSecurityKeysRepository: Symbol('userSecurityKeysRepository'), diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts index bc03aadf90..6c1b92ef16 100644 --- a/packages/backend/src/misc/json-schema.ts +++ b/packages/backend/src/misc/json-schema.ts @@ -11,7 +11,6 @@ import { packedUserDetailedSchema, packedUserLiteSchema, packedUserSchema, - packedUserBannerSchema, } from '@/models/json-schema/user.js'; import { packedNoteSchema } from '@/models/json-schema/note.js'; import { packedUserListSchema } from '@/models/json-schema/user-list.js'; @@ -71,7 +70,6 @@ export const refs = { MeDetailed: packedMeDetailedSchema, UserDetailed: packedUserDetailedSchema, User: packedUserSchema, - UserBanner: packedUserBannerSchema, UserList: packedUserListSchema, UserGroup: packedUserGroupSchema, diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 0925bd86de..a24a3c91f8 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -84,8 +84,6 @@ import { MiUserSecurityKey, MiWebhook, MiOfficialTag, - MiUserBannerPining, - MiUserBanner, } from './_.js'; import type { Provider } from '@nestjs/common'; import type { DataSource } from 'typeorm'; @@ -234,18 +232,6 @@ const $userNotePiningsRepository: Provider = { inject: [DI.db], }; -const $userBannerRepository: Provider = { - provide: DI.userBannerRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserBanner), - inject: [DI.db], -}; - -const $userBannerPiningRepository: Provider = { - provide: DI.userBannerPiningRepository, - useFactory: (db: DataSource) => db.getRepository(MiUserBannerPining), - inject: [DI.db], -}; - const $userIpsRepository: Provider = { provide: DI.userIpsRepository, useFactory: (db: DataSource) => db.getRepository(MiUserIp).extend(miRepository as MiRepository), @@ -585,8 +571,6 @@ const $officialTagRepository: Provider = { $userGroupJoiningsRepository, $userGroupInvitationsRepository, $userNotePiningsRepository, - $userBannerPiningRepository, - $userBannerRepository, $userIpsRepository, $usedUsernamesRepository, $followingsRepository, @@ -665,8 +649,6 @@ const $officialTagRepository: Provider = { $userGroupJoiningsRepository, $userGroupInvitationsRepository, $userNotePiningsRepository, - $userBannerPiningRepository, - $userBannerRepository, $userIpsRepository, $usedUsernamesRepository, $followingsRepository, diff --git a/packages/backend/src/models/UserBanner.ts b/packages/backend/src/models/UserBanner.ts deleted file mode 100644 index 108587b02e..0000000000 --- a/packages/backend/src/models/UserBanner.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Entity, Column, Index, JoinColumn, PrimaryColumn, ManyToOne } from 'typeorm'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; -import { MiDriveFile } from './DriveFile.js'; - -@Entity('user_banner') -export class MiUserBanner { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column('varchar', { - length: 1024, - nullable: true, - }) - public description: string | null; - @Column('varchar', { - length: 1024, - nullable: true, - }) - public url: string | null; - - @Column({ - ...id(), - }) - public fileId: MiDriveFile['id']; - - @ManyToOne(type => MiDriveFile, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public file: MiDriveFile; -} diff --git a/packages/backend/src/models/UserBannerPining.ts b/packages/backend/src/models/UserBannerPining.ts deleted file mode 100644 index 754de012e9..0000000000 --- a/packages/backend/src/models/UserBannerPining.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Entity, Column, Index, JoinColumn, PrimaryColumn, ManyToOne, OneToOne } from 'typeorm'; -import { MiUserBanner } from '@/models/UserBanner.js'; -import { id } from './util/id.js'; -import { MiUser } from './User.js'; - -@Entity('user_banner_pining') -@Index(['userId', 'pinnedBannerId'], { unique: true }) -export class MiUserBannerPining { - @PrimaryColumn(id()) - public id: string; - - @Index() - @Column(id()) - public userId: MiUser['id']; - - @ManyToOne(type => MiUser, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: MiUser | null; - - @Column({ - ...id(), - }) - public pinnedBannerId: MiUserBanner['id']; - - @ManyToOne(type => MiUserBanner, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public pinnedBanner: MiUserBanner; -} diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index ad9cbc1df6..7d72b53a58 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -9,6 +9,7 @@ import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiPage } from './Page.js'; import { MiUserList } from './UserList.js'; +import type { MiDriveFile } from './DriveFile.js'; // TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも // ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン @@ -42,6 +43,18 @@ export class MiUserProfile { }) public description: string | null; + @Column('jsonb', { + default: [], + }) + public mutualLinkSections: { + name: string | null; + mutualLinks: { + fileId: MiDriveFile['id']; + description: string | null; + imgSrc: string; + }[]; + }[] | []; + @Column('jsonb', { default: [], }) diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 7ff7dbb94f..f44fd5f4ec 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -72,8 +72,6 @@ import { MiUserListMembership } from '@/models/UserListMembership.js'; import { MiUserNotePining } from '@/models/UserNotePining.js'; import { MiUserPending } from '@/models/UserPending.js'; import { MiUserProfile } from '@/models/UserProfile.js'; -import { MiUserBanner } from '@/models/UserBanner.js'; -import { MiUserBannerPining } from '@/models/UserBannerPining.js'; import { MiUserPublickey } from '@/models/UserPublickey.js'; import { MiUserSecurityKey } from '@/models/UserSecurityKey.js'; import { MiUserMemo } from '@/models/UserMemo.js'; @@ -197,8 +195,6 @@ export { MiUserNotePining, MiUserPending, MiUserProfile, - MiUserBanner, - MiUserBannerPining, MiUserPublickey, MiUserSecurityKey, MiWebhook, diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 6fd4c26dec..b3b15d5993 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -284,6 +284,14 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, + mutualLinkSectionLimit: { + type: 'integer', + optional: false, nullable: false, + }, + mutualLinkLimit: { + type: 'integer', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 54dbb37e49..0433122f16 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -390,6 +390,29 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'string', nullable: false, optional: true, }, + mutualLinkSections: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string', nullable: true }, + mutualLinks: { + type: 'array', + items: { + type: 'object', + properties: { + url: { type: 'string' }, + fileId: { type: 'string', format: 'misskey:id' }, + description: { type: 'string', nullable: true }, + imgSrc: { type: 'string' }, + }, + required: ['url', 'fileId'], + }, + }, + }, + required: ['mutualLinks'], + }, + }, //#region relations isFollowing: { type: 'boolean', @@ -432,80 +455,6 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'boolean', nullable: false, optional: true, }, - mutualBanners: { - type: 'array', - nullable: true, optional: false, - items: { - type: 'object', - nullable: false, optional: false, - properties: { - id: { - type: 'string', - format: 'id', - nullable: false, optional: false, - }, - user: { - type: 'object', - nullable: false, optional: false, - ref: 'UserLite', - }, - description: { - type: 'string', - nullable: true, optional: false, - }, - imgUrl: { - type: 'string', - format: 'url', - nullable: false, optional: false, - }, - url: { - type: 'string', - format: 'url', - nullable: false, optional: false, - }, - fileId: { - type: 'string', - format: 'id', - nullable: false, optional: false, - }, - }, - }, - }, - myMutualBanner: { - type: 'object', - nullable: true, optional: false, - properties: { - id: { - type: 'string', - format: 'id', - nullable: false, optional: false, - }, - user: { - type: 'object', - nullable: false, optional: false, - ref: 'UserLite', - }, - description: { - type: 'string', - nullable: true, optional: false, - }, - imgUrl: { - type: 'string', - format: 'url', - nullable: false, optional: false, - }, - url: { - type: 'string', - format: 'url', - nullable: false, optional: false, - }, - fileId: { - type: 'string', - format: 'id', - nullable: false, optional: false, - }, - }, - }, //#endregion }, } as const; @@ -808,37 +757,3 @@ export const packedUserSchema = { }, ], } as const; - -export const packedUserBannerSchema = { - type: 'object', - properties: { - id: { - type: 'string', - nullable: false, optional: false, - format: 'id', - }, - user: { - type: 'object', - nullable: false, optional: false, - ref: 'UserLite', - }, - description: { - type: 'string', - nullable: true, optional: false, - }, - imgUrl: { - type: 'string', - format: 'url', - nullable: false, optional: false, - }, - url: { - type: 'string', - nullable: true, optional: false, - }, - fileId: { - type: 'string', - format: 'id', - nullable: false, optional: false, - }, - }, -} as const; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 400e559e83..22c637be26 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -85,8 +85,6 @@ import { MiFlashLike } from '@/models/FlashLike.js'; import { MiUserMemo } from '@/models/UserMemo.js'; import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js'; import { MiReversiGame } from '@/models/ReversiGame.js'; -import { MiUserBanner } from '@/models/UserBanner.js'; -import { MiUserBannerPining } from '@/models/UserBannerPining.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; @@ -212,8 +210,6 @@ export const entities = [ MiFlash, MiFlashLike, MiUserMemo, - MiUserBanner, - MiUserBannerPining, MiBubbleGameRecord, MiReversiGame, ...charts, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index b3811fa1f6..b92b8d3d4c 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -36,7 +36,7 @@ import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-d import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; -import * as ep___admin_unsetUserMutualBanner from './endpoints/admin/unset-user-mutual-banner.js'; +import * as ep___admin_unsetUserMutualLink from './endpoints/admin/unset-user-mutual-link.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -445,7 +445,7 @@ const $admin_avatarDecorations_update: Provider = { provide: 'ep:admin/avatar-de const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default }; const $admin_unsetUserAvatar: Provider = { provide: 'ep:admin/unset-user-avatar', useClass: ep___admin_unsetUserAvatar.default }; const $admin_unsetUserBanner: Provider = { provide: 'ep:admin/unset-user-banner', useClass: ep___admin_unsetUserBanner.default }; -const $admin_unsetUserMutualBanner: Provider = { provide: 'ep:admin/unset-user-mutual-banner', useClass: ep___admin_unsetUserMutualBanner.default }; +const $admin_unsetUserMutualLink: Provider = { provide: 'ep:admin/unset-user-mutual-link', useClass: ep___admin_unsetUserMutualLink.default }; const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default }; const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default }; const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default }; @@ -859,7 +859,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, $admin_unsetUserBanner, - $admin_unsetUserMutualBanner, + $admin_unsetUserMutualLink, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, @@ -1266,7 +1266,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $admin_deleteAllFilesOfAUser, $admin_unsetUserAvatar, $admin_unsetUserBanner, - $admin_unsetUserMutualBanner, + $admin_unsetUserMutualLink, $admin_drive_cleanRemoteFiles, $admin_drive_cleanup, $admin_drive_files, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 7ae16d55ce..2bfaa3b556 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -40,7 +40,7 @@ import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-d import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js'; import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js'; import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js'; -import * as ep___admin_unsetUserMutualBanner from './endpoints/admin/unset-user-mutual-banner.js'; +import * as ep___admin_unsetUserMutualLink from './endpoints/admin/unset-user-mutual-link.js'; import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js'; import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js'; import * as ep___admin_drive_files from './endpoints/admin/drive/files.js'; @@ -448,7 +448,7 @@ const eps = [ ['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser], ['admin/unset-user-avatar', ep___admin_unsetUserAvatar], ['admin/unset-user-banner', ep___admin_unsetUserBanner], - ['admin/unset-user-mutual-banner', ep___admin_unsetUserMutualBanner], + ['admin/unset-user-mutual-link', ep___admin_unsetUserMutualLink], ['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles], ['admin/drive/cleanup', ep___admin_drive_cleanup], ['admin/drive/files', ep___admin_drive_files], diff --git a/packages/backend/src/server/api/endpoints/admin/unset-user-mutual-banner.ts b/packages/backend/src/server/api/endpoints/admin/unset-user-mutual-link.ts similarity index 61% rename from packages/backend/src/server/api/endpoints/admin/unset-user-mutual-banner.ts rename to packages/backend/src/server/api/endpoints/admin/unset-user-mutual-link.ts index 139a5e0204..735711c0e0 100644 --- a/packages/backend/src/server/api/endpoints/admin/unset-user-mutual-banner.ts +++ b/packages/backend/src/server/api/endpoints/admin/unset-user-mutual-link.ts @@ -1,5 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; -import type { UserBannerRepository, UsersRepository } from '@/models/_.js'; +import type { + UsersRepository, + UserProfilesRepository, +} from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -9,7 +12,7 @@ export const meta = { requireCredential: true, requireModerator: true, - kind: 'write:admin:unset-user-mutual-banner', + kind: 'write:admin:unset-user-mutual-link', } as const; export const paramDef = { @@ -26,32 +29,27 @@ export default class extends Endpoint { constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, - @Inject(DI.userBannerRepository) - private userBannerRepository: UserBannerRepository, private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy({ id: ps.userId }); + const userProfile = await this.userProfilesRepository.findOneBy({ userId: ps.userId }); - if (user == null) { + if (user == null || userProfile == null) { throw new Error('user not found'); } - const mutualBanner = await this.userBannerRepository.findOneBy({ userId: user.id }); - - if (mutualBanner == null) return; - - await this.userBannerRepository.delete({ - id: mutualBanner.id, + await this.userProfilesRepository.update(user.id, { + mutualLinkSections: [], }); - this.moderationLogService.log(me, 'unsetUserMutualBanner', { + this.moderationLogService.log(me, 'unsetUserMutualLink', { userId: user.id, userUsername: user.username, - userBannerDescription: mutualBanner.description, - userBannerUrl: mutualBanner.url, - fileId: mutualBanner.fileId, + userMutualLinkSections: userProfile.mutualLinkSections, }); }); } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 9ec6d766fa..11a0e28e09 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -11,14 +11,7 @@ import { JSDOM } from 'jsdom'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import * as Acct from '@/misc/acct.js'; -import type { - UsersRepository, - DriveFilesRepository, - UserProfilesRepository, - PagesRepository, - UserBannerRepository, - UserBannerPiningRepository, -} from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js'; import type { MiUserProfile } from '@/models/UserProfile.js'; @@ -41,8 +34,6 @@ import type { Config } from '@/config.js'; import { safeForSql } from '@/misc/safe-for-sql.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { notificationRecieveConfig } from '@/models/json-schema/user.js'; -import { UserBannerService } from '@/core/UserBannerService.js'; -import { UserBannerPiningService } from '@/core/UserBannerPiningService.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; @@ -238,23 +229,27 @@ export const paramDef = { uniqueItems: true, items: { type: 'string' }, }, - mutualBannerPining: { + mutualLinkSections: { type: 'array', - nullable: true, items: { - type: 'string', - format: 'misskey:id', - }, - }, - myMutualBanner: { - type: 'object', - nullable: true, - properties: { - fileId: { type: 'string', format: 'misskey:id' }, - description: { type: 'string' }, - url: { type: 'string', nullable: true, format: 'url' }, + type: 'object', + properties: { + name: { type: 'string', nullable: true }, + mutualLinks: { + type: 'array', + items: { + type: 'object', + properties: { + url: { type: 'string', format: 'url' }, + fileId: { type: 'string', format: 'misskey:id' }, + description: { type: 'string', nullable: true }, + }, + required: ['url', 'fileId'], + }, + }, + }, + required: ['mutualLinks'], }, - required: ['fileId'], }, }, } as const; @@ -274,17 +269,10 @@ export default class extends Endpoint { // eslint- @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - @Inject(DI.userBannerRepository) - private userBannerRepository: UserBannerRepository, - @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, - @Inject(DI.userBannerPiningRepository) - private userBannerPiningRepository: UserBannerPiningRepository, - private userEntityService: UserEntityService, - private userBannerService: UserBannerService, private driveFileEntityService: DriveFileEntityService, private globalEventService: GlobalEventService, private userFollowingService: UserFollowingService, @@ -296,7 +284,6 @@ export default class extends Endpoint { // eslint- private cacheService: CacheService, private httpRequestService: HttpRequestService, private avatarDecorationService: AvatarDecorationService, - private userBannerPiningService: UserBannerPiningService, ) { super(meta, paramDef, async (ps, _user, token, flashToken) => { const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser; @@ -400,48 +387,41 @@ export default class extends Endpoint { // eslint- updates.avatarBlurhash = null; } - if (ps.mutualBannerPining) { - const bannerPiningNow = await this.userBannerPiningRepository.findBy({ userId: user.id }); - - const bannerPiningNowIds = new Set(bannerPiningNow.map(b => b.pinnedBannerId)); - const mutualBannerPiningIds = new Set(ps.mutualBannerPining); - - const bannersToAdd = [...mutualBannerPiningIds].filter(bannerId => !bannerPiningNowIds.has(bannerId)); - const bannersToRemove = [...bannerPiningNowIds].filter(bannerId => !mutualBannerPiningIds.has(bannerId)); - - if (bannersToAdd.length > 0) { - await this.userBannerPiningService.addPinned(user.id, bannersToAdd); + if (ps.mutualLinkSections) { + if (ps.mutualLinkSections.length > policy.mutualLinkSectionLimit) { + throw new ApiError(meta.errors.restrictedByRole); } - if (bannersToRemove.length > 0) { - await this.userBannerPiningService.removePinned(user.id, bannersToRemove); - } - } + const mutualLinkSections = ps.mutualLinkSections.map(async (section) => { + if (section.mutualLinks.length > policy.mutualLinkLimit) { + throw new ApiError(meta.errors.restrictedByRole); + } - if (ps.myMutualBanner) { - const banner = await this.userBannerRepository.findOneBy({ - userId: user.id, + const mutualLinks = await Promise.all(section.mutualLinks.map(async (mutualLink) => { + const file = await this.driveFilesRepository.findOneBy({ id: mutualLink.fileId }); + + if (!file) { + throw new ApiError(meta.errors.noSuchFile); + } + if (!file.type.startsWith('image/')) { + throw new ApiError(meta.errors.fileNotAnImage); + } + + return { + url: mutualLink.url, + fileId: file.id, + imgSrc: this.driveFileEntityService.getPublicUrl(file), + description: mutualLink.description ?? null, + }; + })); + + return { + name: section.name ?? null, + mutualLinks, + }; }); - const file = await this.driveFilesRepository.findOneBy({ id: ps.myMutualBanner.fileId }); - const profileUrl = this.config.url + '/@' + user.username; - - if (file === null) throw new ApiError(meta.errors.noSuchFile); - if (!file.type.startsWith('image/')) throw new ApiError(meta.errors.fileNotAnImage); - if (banner) { - await this.userBannerService.update(user.id, banner.id, ps.myMutualBanner.description ?? null, ps.myMutualBanner.url ?? profileUrl, ps.myMutualBanner.fileId); - } else { - await this.userBannerService.create(user.id, ps.myMutualBanner.description ?? null, ps.myMutualBanner.url ?? profileUrl, ps.myMutualBanner.fileId); - } - } - - if (ps.myMutualBanner === null) { - const banner = await this.userBannerRepository.findOneBy({ - userId: user.id, - }); - if (banner) { - await this.userBannerService.delete(user.id, banner.id); - } + profileUpdates.mutualLinkSections = await Promise.all(mutualLinkSections); } if (ps.bannerId) { diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 1c3838bc9e..c295f1cc31 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -99,7 +99,7 @@ export const moderationLogTypes = [ 'updateAbuseReportNotificationRecipient', 'deleteAbuseReportNotificationRecipient', 'updateOfficialTags', - 'unsetUserMutualBanner', + 'unsetUserMutualLink', ] as const; export type ModerationLogPayloads = { @@ -327,12 +327,10 @@ export type ModerationLogPayloads = { priority: number, }[]; }; - unsetUserMutualBanner: { + unsetUserMutualLink: { userId: string; userUsername: string; - userBannerDescription: string | null; - userBannerUrl: string | null; - fileId: string; + userMutualLinkSections: { name: string | null; mutualLinks: { fileId: string; description: string | null; imgSrc: string; }[]; }[] | [] } }; diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index 6e2630f3a6..747a3314e6 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -73,8 +73,7 @@ describe('ユーザー', () => { lang: user.lang, fields: user.fields, verifiedLinks: user.verifiedLinks, - myMutualBanner: user.myMutualBanner, - mutualBanners: user.mutualBanners, + mutualLinkSections: user.mutualLinkSections, followersCount: user.followersCount, followingCount: user.followingCount, notesCount: user.notesCount, diff --git a/packages/cherrypick-js/etc/cherrypick-js.api.md b/packages/cherrypick-js/etc/cherrypick-js.api.md index 08d7b71364..81fe72146d 100644 --- a/packages/cherrypick-js/etc/cherrypick-js.api.md +++ b/packages/cherrypick-js/etc/cherrypick-js.api.md @@ -401,7 +401,7 @@ type AdminUnsetUserAvatarRequest = operations['admin___unset-user-avatar']['requ type AdminUnsetUserBannerRequest = operations['admin___unset-user-banner']['requestBody']['content']['application/json']; // @public (undocumented) -type AdminUnsetUserMutualBannerRequest = operations['admin___unset-user-mutual-banner']['requestBody']['content']['application/json']; +type AdminUnsetUserMutualLinkRequest = operations['admin___unset-user-mutual-link']['requestBody']['content']['application/json']; // @public (undocumented) type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json']; @@ -1236,7 +1236,7 @@ declare namespace entities { AdminDeleteAllFilesOfAUserRequest, AdminUnsetUserAvatarRequest, AdminUnsetUserBannerRequest, - AdminUnsetUserMutualBannerRequest, + AdminUnsetUserMutualLinkRequest, AdminDriveFilesRequest, AdminDriveFilesResponse, AdminDriveShowFileRequest, @@ -1803,7 +1803,6 @@ declare namespace entities { MeDetailed, UserDetailed, User, - UserBanner, UserList, UserGroup, Ad, @@ -2557,7 +2556,7 @@ type ModerationLog = { }); // @public (undocumented) -export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "unsetUserMutualBanner"]; +export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient"]; // @public (undocumented) type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json']; @@ -2839,7 +2838,7 @@ type PartialRolePolicyOverride = Partial<{ }>; // @public (undocumented) -export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unset-user-mutual-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:admin:official-tags", "write:admin:reindex"]; +export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:admin:official-tags", "write:admin:reindex"]; // @public (undocumented) type PingResponse = operations['ping']['responses']['200']['content']['application/json']; @@ -3141,9 +3140,6 @@ function toString_2(acct: Acct): string; // @public (undocumented) type User = components['schemas']['User']; -// @public (undocumented) -type UserBanner = components['schemas']['UserBanner']; - // @public (undocumented) type UserDetailed = components['schemas']['UserDetailed']; diff --git a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts index 082ddfa02d..b9459f796b 100644 --- a/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts +++ b/packages/cherrypick-js/src/autogen/apiClientJSDoc.ts @@ -334,9 +334,9 @@ declare module '../api.js' { /** * No description provided. * - * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-banner* + * **Credential required**: *Yes* / **Permission**: *write:admin:unset-user-mutual-link* */ - request( + request( endpoint: E, params: P, credential?: string | null, diff --git a/packages/cherrypick-js/src/autogen/endpoint.ts b/packages/cherrypick-js/src/autogen/endpoint.ts index eb5a563b6f..006e09f465 100644 --- a/packages/cherrypick-js/src/autogen/endpoint.ts +++ b/packages/cherrypick-js/src/autogen/endpoint.ts @@ -44,7 +44,7 @@ import type { AdminDeleteAllFilesOfAUserRequest, AdminUnsetUserAvatarRequest, AdminUnsetUserBannerRequest, - AdminUnsetUserMutualBannerRequest, + AdminUnsetUserMutualLinkRequest, AdminDriveFilesRequest, AdminDriveFilesResponse, AdminDriveShowFileRequest, @@ -635,7 +635,7 @@ export type Endpoints = { 'admin/delete-all-files-of-a-user': { req: AdminDeleteAllFilesOfAUserRequest; res: EmptyResponse }; 'admin/unset-user-avatar': { req: AdminUnsetUserAvatarRequest; res: EmptyResponse }; 'admin/unset-user-banner': { req: AdminUnsetUserBannerRequest; res: EmptyResponse }; - 'admin/unset-user-mutual-banner': { req: AdminUnsetUserMutualBannerRequest; res: EmptyResponse }; + 'admin/unset-user-mutual-link': { req: AdminUnsetUserMutualLinkRequest; res: EmptyResponse }; 'admin/drive/clean-remote-files': { req: EmptyRequest; res: EmptyResponse }; 'admin/drive/cleanup': { req: EmptyRequest; res: EmptyResponse }; 'admin/drive/files': { req: AdminDriveFilesRequest; res: AdminDriveFilesResponse }; @@ -1043,7 +1043,7 @@ export const endpointReqTypes: Record {{ i18n.ts.resetPassword }} {{ i18n.ts.unsetUserAvatar }} {{ i18n.ts.unsetUserBanner }} - {{ i18n.ts.unsetUserMutualBanner }} + {{ i18n.ts.unsetUserMutualLink }}
@@ -365,10 +365,10 @@ async function unsetUserBanner() { refreshUser(); } -async function unsetUserMutualBanner() { +async function unsetUserMutualLink() { const confirm = await os.confirm({ type: 'warning', - text: i18n.ts.unsetUserMutualBannerConfirm, + text: i18n.ts.unsetUserMutualLinkConfirm, }); if (confirm.canceled) return; diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index c9a28a3358..7f1fa93aba 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -592,6 +592,44 @@ SPDX-License-Identifier: AGPL-3.0-only + + + +
+ + + + + + + + +
+
+ + + + +
+ + + + + + + + +
+
+ From da877533a44f8fc8fcc7a4b6d5ed63106ae659a5 Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Wed, 14 Aug 2024 22:53:32 +0900 Subject: [PATCH 03/12] =?UTF-8?q?=E3=83=93=E3=83=AB=E3=83=89=E3=81=8C?= =?UTF-8?q?=E9=80=9A=E3=82=89=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 4 ++-- packages/frontend/src/pages/user/home.vue | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index 683fff4f6c..96aa22c4b4 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -2827,11 +2827,11 @@ export interface Locale extends ILocale { */ "unsetUserBannerConfirm": string; /** - * 相互リンクを解除 + * 相互リンクを削除 */ "unsetUserMutualLink": string; /** - * 相互リンクを解除しますか? + * 相互リンクを削除しますか? */ "unsetUserMutualLinkConfirm": string; /** diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 1c53e5a1b5..cdbc237fe3 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
{{ number(user.notesCount) }} From 0cf9d60186fe0cef46eb381913aab28b55f6cc8a Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:02:01 +0900 Subject: [PATCH 04/12] fix roleService --- packages/backend/src/server/api/endpoints/i/update.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 11a0e28e09..870dc9bb18 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -388,6 +388,7 @@ export default class extends Endpoint { // eslint- } if (ps.mutualLinkSections) { + const policy = await this.roleService.getUserPolicies(user.id); if (ps.mutualLinkSections.length > policy.mutualLinkSectionLimit) { throw new ApiError(meta.errors.restrictedByRole); } From 82dcaeb23665da365d9e28bc92f941801ff98a97 Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:08:37 +0900 Subject: [PATCH 05/12] =?UTF-8?q?enhance(profile):=20=E7=9B=B8=E4=BA=92?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF=E6=A9=9F=E8=83=BD=E3=81=AE=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A=E7=94=BB=E9=9D=A2=E3=81=AE=E3=83=87=E3=82=B6=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E4=BF=AE=E6=AD=A3=20(MisskeyIO#690)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 90be6317e6cd39e82bc0b1a108fe490f0fb772a9) # Conflicts: # locales/en-US.yml # locales/ko-KR.yml Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com> --- locales/en-US.yml | 10 ++++++++++ locales/index.d.ts | 2 +- locales/ja-JP.yml | 2 +- locales/ko-KR.yml | 10 ++++++++++ packages/frontend/src/pages/settings/profile.vue | 9 +++++---- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index dd18930bed..f991d1292b 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2541,6 +2541,16 @@ _profile: changeBanner: "Change banner" verifiedLinkDescription: "By entering an URL that contains a link to your profile here, an ownership verification icon can be displayed next to the field." avatarDecorationMax: "You can add up to {max} decorations." + mutualLinksEdit: "Edit mutual links" + mutualLinksBanner: "Banner of mutual links" + mutualLinksDescriptionEdit: "Description" + mutualLinksUrl: "URL of the link" + mutualLinksDescription: "Mutual links are displayed as banners on your profile." + addMutualLink: "Add mutual link" + addMutualLinkSection: "Add section" + sectionName: "Section name" + sectionNameNoneDescription: "Do not display the section name" + sectionNameNone: "Section without name" _exportOrImport: allNotes: "All notes" favoritedNotes: "Favorite notes" diff --git a/locales/index.d.ts b/locales/index.d.ts index 96aa22c4b4..530215acac 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10090,7 +10090,7 @@ export interface Locale extends ILocale { */ "sectionNameNoneDescription": string; /** - * セクション名を表示しない + * 名前が表示されないセクション */ "sectionNameNone": string; }; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 05b634eef0..cbe7ff4bfa 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2659,7 +2659,7 @@ _profile: addMutualLinkSection: "セクションを追加" sectionName: "セクション名" sectionNameNoneDescription: "セクション名を表示しないようにする" - sectionNameNone: "セクション名を表示しない" + sectionNameNone: "名前が表示されないセクション" _exportOrImport: allNotes: "全てのノート" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index f439613b60..b71a13bbfd 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -2522,6 +2522,16 @@ _profile: changeBanner: "배너 이미지 변경" verifiedLinkDescription: "내용에 자신의 프로필로 향하는 링크가 포함된 페이지의 URL을 삽입하면 소유자 인증 마크가 표시돼요." avatarDecorationMax: "최대 {max}개까지 장식을 달 수 있어요." + mutualLinksEdit: "서로링크 편집" + mutualLinksBanner: "서로링크 배너" + mutualLinksDescriptionEdit: "설명" + mutualLinksUrl: "링크 URL" + mutualLinksDescription: "서로링크를 설정하면 프로필에 배너가 표시됩니다." + addMutualLink: "서로링크 추가" + addMutualLinkSection: "섹션 추가" + sectionName: "섹션 이름" + sectionNameNoneDescription: "섹션 이름이 표시되지 않도록 합니다." + sectionNameNone: "이름이 표시되지 않는 섹션" _exportOrImport: allNotes: "모든 노트" favoritedNotes: "즐겨찾기한 노트" diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 6d67cf833d..035a28e25a 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -117,11 +117,12 @@ SPDX-License-Identifier: AGPL-3.0-only -
- - {{ i18n.ts._profile.sectionNameNoneDescription }} +
+ + {{ i18n.ts._profile.sectionNameNoneDescription }} {{ i18n.ts._profile.addMutualLink }}
+ Date: Thu, 15 Aug 2024 01:19:11 +0900 Subject: [PATCH 06/12] =?UTF-8?q?mutualLinkSections=E3=82=92activitypub?= =?UTF-8?q?=E3=81=A7=E8=BB=A2=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/activitypub/ApRendererService.ts | 19 ++++++++ .../activitypub/models/ApPersonService.ts | 48 ++++++++++++++++++- packages/backend/src/core/activitypub/type.ts | 10 ++++ packages/backend/src/models/UserProfile.ts | 1 + 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index f37458256a..493153c559 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -544,6 +544,25 @@ export class ApRendererService { person['vcard:Address'] = profile.location; } + if (profile.mutualLinkSections.length > 0) { + const ApMutualLinkSections=await Promise.all(profile.mutualLinkSections.map(async section=>{ + return { + sectionName: section.name ? this.mfmService.toHtml(mfm.parse(section.name)) : null, + _misskey_sectionName: section.name, + entrys: await Promise.all(section.mutualLinks.map(async entry=>{ + let img=await this.driveFilesRepository.findOneBy({ id: entry.fileId }); + return { + description: entry.description ? this.mfmService.toHtml(mfm.parse(entry.description)) : null, + _misskey_description: entry.description, + image: img ? this.renderImage(img) : null, + url: entry.url, + } + })), + } + })); + person.mutualLinkSections = ApMutualLinkSections; + } + return person; } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 72c40bf2d3..41b93090ef 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -8,7 +8,7 @@ import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; +import type { FollowingsRepository, InstancesRepository, MiDriveFile, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js'; @@ -47,7 +47,7 @@ import type { ApNoteService } from './ApNoteService.js'; import type { ApMfmService } from '../ApMfmService.js'; import type { ApResolverService, Resolver } from '../ApResolverService.js'; import type { ApLoggerService } from '../ApLoggerService.js'; -// eslint-disable-next-line @typescript-eslint/consistent-type-imports + import type { ApImageService } from './ApImageService.js'; import type { IActor, IObject } from '../type.js'; @@ -448,6 +448,7 @@ export class ApPersonService implements OnModuleInit { birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, userHost: host, + mutualLinkSections: await this.mutualLinkSections(person, user), })); if (person.publicKey) { @@ -696,6 +697,7 @@ export class ApPersonService implements OnModuleInit { description: _description, birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, + mutualLinkSections: await this.mutualLinkSections(person, exist), }); this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: exist.id }); @@ -736,6 +738,48 @@ export class ApPersonService implements OnModuleInit { return 'skip'; } + async mutualLinkSections(person: IActor, actor: MiRemoteUser) : Promise<[] | { + name: string | null; + mutualLinks: { + fileId: MiDriveFile['id']; + description: string | null; + imgSrc: string; + url: string; + }[]; +}[]> { + const apMutualLinkSections = person.mutualLinkSections; + + if (apMutualLinkSections === undefined) return []; + + return await Promise.all(apMutualLinkSections.map(async ap => { + let name = null; + if (ap._misskey_sectionName) { + name = truncate(ap._misskey_sectionName, summaryLength); + } else if (ap.sectionName) { + name = this.apMfmService.htmlToMfm(truncate(ap.sectionName, summaryLength), person.tag); + } + return { + name, + mutualLinks: (await Promise.all(ap.entrys.map(async entry => { + if (entry.url === null) return null; + const image = entry.image ? await this.apImageService.resolveImage(actor, entry.image).catch(() => null) : null; + if (image === null) return null; + let description = null; + if (entry._misskey_description) { + description = truncate(entry._misskey_description, summaryLength); + } else if (entry.description) { + description = this.apMfmService.htmlToMfm(truncate(entry.description, summaryLength), person.tag); + } + return { + fileId: image.id, + imgSrc: image.url, + url: entry.url, + description, + }; + }))).filter(e => e !== null), + }; + })); + } /** * Personを解決します。 diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 1050c9fcfd..0080c98395 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -198,6 +198,16 @@ export interface IActor extends IObject { }; 'vcard:bday'?: string; 'vcard:Address'?: string; + mutualLinkSections?: { + sectionName?: string | null; + _misskey_sectionName?: string | null; + entrys: { + description?: string | null; + _misskey_description: string | null; + image: string | IObject | null;//ap image + url: string | null;//link to + }[] | []; + }[]; } export const isCollection = (object: IObject): object is ICollection => diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 7d72b53a58..e68ffd9737 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -52,6 +52,7 @@ export class MiUserProfile { fileId: MiDriveFile['id']; description: string | null; imgSrc: string; + url: string; }[]; }[] | []; From 30f1bf7ed7a21d71076a4819f7847e46ddc56f31 Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Thu, 15 Aug 2024 01:38:19 +0900 Subject: [PATCH 07/12] =?UTF-8?q?mutualLink.imgSrc=E3=82=92=E3=83=A1?= =?UTF-8?q?=E3=83=87=E3=82=A3=E3=82=A2=E3=83=97=E3=83=AD=E3=82=AD=E3=82=B7?= =?UTF-8?q?=E7=B5=8C=E7=94=B1=E3=81=A7=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/pages/user/home.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index cdbc237fe3..19a5749f47 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -103,7 +103,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -203,7 +203,7 @@ import { confetti } from '@/scripts/confetti.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; import { useRouter } from '@/router/supplier.js'; -import { getStaticImageUrl } from '@/scripts/media-proxy.js'; +import { getStaticImageUrl, getProxiedImageUrl } from '@/scripts/media-proxy.js'; import { miLocalStorage } from '@/local-storage.js'; import { editNickname } from '@/scripts/edit-nickname.js'; import { vibrate } from '@/scripts/vibrate.js'; From fbaac8d1e5b378375f8614fdbfe00f468b2bf4a0 Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Thu, 15 Aug 2024 02:39:05 +0900 Subject: [PATCH 08/12] =?UTF-8?q?AP=E4=B8=8A=E3=81=A7=E8=BB=A2=E9=80=81?= =?UTF-8?q?=E3=81=99=E3=82=8BmutualLinkSections=E3=82=92banner=E3=81=AB?= =?UTF-8?q?=E3=83=AA=E3=83=8D=E3=83=BC=E3=83=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/activitypub/ApRendererService.ts | 2 +- packages/backend/src/core/activitypub/models/ApPersonService.ts | 2 +- packages/backend/src/core/activitypub/type.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 493153c559..a1b400d520 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -560,7 +560,7 @@ export class ApRendererService { })), } })); - person.mutualLinkSections = ApMutualLinkSections; + person.banner = ApMutualLinkSections; } return person; diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 41b93090ef..a3b2d1fce2 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -747,7 +747,7 @@ export class ApPersonService implements OnModuleInit { url: string; }[]; }[]> { - const apMutualLinkSections = person.mutualLinkSections; + const apMutualLinkSections = person.banner; if (apMutualLinkSections === undefined) return []; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 0080c98395..f44f722e3d 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -198,7 +198,7 @@ export interface IActor extends IObject { }; 'vcard:bday'?: string; 'vcard:Address'?: string; - mutualLinkSections?: { + banner?: { sectionName?: string | null; _misskey_sectionName?: string | null; entrys: { From cf66376e224d239febefc44a54c7e736c80b0662 Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Thu, 15 Aug 2024 02:49:02 +0900 Subject: [PATCH 09/12] fix lint --- .../backend/src/core/activitypub/ApRendererService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index a1b400d520..c6612c2947 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -545,20 +545,20 @@ export class ApRendererService { } if (profile.mutualLinkSections.length > 0) { - const ApMutualLinkSections=await Promise.all(profile.mutualLinkSections.map(async section=>{ + const ApMutualLinkSections = await Promise.all(profile.mutualLinkSections.map(async section => { return { sectionName: section.name ? this.mfmService.toHtml(mfm.parse(section.name)) : null, _misskey_sectionName: section.name, - entrys: await Promise.all(section.mutualLinks.map(async entry=>{ - let img=await this.driveFilesRepository.findOneBy({ id: entry.fileId }); + entrys: await Promise.all(section.mutualLinks.map(async entry => { + const img = await this.driveFilesRepository.findOneBy({ id: entry.fileId }); return { description: entry.description ? this.mfmService.toHtml(mfm.parse(entry.description)) : null, _misskey_description: entry.description, image: img ? this.renderImage(img) : null, url: entry.url, - } + }; })), - } + }; })); person.banner = ApMutualLinkSections; } From 5921c6d5bee82f6fc72caa17baaac0ea7089777f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:20:07 +0900 Subject: [PATCH 10/12] =?UTF-8?q?spec(profile):=20=E7=9B=B8=E4=BA=92?= =?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF=E3=83=90=E3=83=8A=E3=83=BC=E3=81=AE?= =?UTF-8?q?=E3=82=B5=E3=82=A4=E3=82=BA=E5=A4=89=E6=9B=B4=E3=83=BBID?= =?UTF-8?q?=E4=BB=98=E4=B8=8E=20(MisskeyIO#696)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/models/UserProfile.ts | 1 + packages/backend/src/models/json-schema/user.ts | 3 ++- .../backend/src/server/api/endpoints/i/update.ts | 3 +++ packages/cherrypick-js/src/autogen/types.ts | 2 ++ packages/frontend/src/pages/settings/profile.vue | 12 +++++++----- packages/frontend/src/pages/user/home.vue | 6 +++--- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index e68ffd9737..38cffc4a4c 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -49,6 +49,7 @@ export class MiUserProfile { public mutualLinkSections: { name: string | null; mutualLinks: { + id: string; fileId: MiDriveFile['id']; description: string | null; imgSrc: string; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 0433122f16..ec43ef12e3 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -401,12 +401,13 @@ export const packedUserDetailedNotMeOnlySchema = { items: { type: 'object', properties: { + id: { type: 'string', format: 'misskey:id' }, url: { type: 'string' }, fileId: { type: 'string', format: 'misskey:id' }, description: { type: 'string', nullable: true }, imgSrc: { type: 'string' }, }, - required: ['url', 'fileId'], + required: ['id', 'url', 'fileId'], }, }, }, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 870dc9bb18..36479ffc4c 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -36,6 +36,7 @@ import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { notificationRecieveConfig } from '@/models/json-schema/user.js'; import { ApiLoggerService } from '../../ApiLoggerService.js'; import { ApiError } from '../../error.js'; +import { IdService } from "@/core/IdService.js"; export const meta = { tags: ['account'], @@ -272,6 +273,7 @@ export default class extends Endpoint { // eslint- @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, + private idService: IdService, private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, private globalEventService: GlobalEventService, @@ -409,6 +411,7 @@ export default class extends Endpoint { // eslint- } return { + id: this.idService.gen(), url: mutualLink.url, fileId: file.id, imgSrc: this.driveFileEntityService.getPublicUrl(file), diff --git a/packages/cherrypick-js/src/autogen/types.ts b/packages/cherrypick-js/src/autogen/types.ts index 3cd49a083b..ceebaf7b8a 100644 --- a/packages/cherrypick-js/src/autogen/types.ts +++ b/packages/cherrypick-js/src/autogen/types.ts @@ -4017,6 +4017,8 @@ export type components = { mutualLinkSections: ({ name: string | null; mutualLinks: ({ + /** Format: misskey:id */ + id: string; url: string; /** Format: misskey:id */ fileId: string; diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 035a28e25a..592207a89a 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -183,7 +183,7 @@ SPDX-License-Identifier: AGPL-3.0-only