From d0ee0203e12d41515b05d7d99ae09f70eeae5874 Mon Sep 17 00:00:00 2001 From: Ryu jongheon Date: Tue, 18 Jun 2024 12:18:04 +0900 Subject: Fix(backend): Limit antenna/webhook/list to exact amount (#14036) ... not +1 * Update antennas/clips e2e test --- packages/backend/src/server/api/endpoints/i/import-antennas.ts | 2 +- packages/backend/src/server/api/endpoints/i/webhooks/create.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/i') diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts index b4661a93e2..bc46163e3d 100644 --- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts +++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts @@ -78,7 +78,7 @@ export default class extends Endpoint { if (file.size === 0) throw new ApiError(meta.errors.emptyFile); const antennas: (_Antenna & { userListAccts: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url)); const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id }); - if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { + if (currentAntennasCount + antennas.length >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas); } this.queueService.createImportAntennasJob(me, antennas); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts index c692380288..9eb7f5b3a0 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts @@ -85,7 +85,7 @@ export default class extends Endpoint { // eslint- const currentWebhooksCount = await this.webhooksRepository.countBy({ userId: me.id, }); - if (currentWebhooksCount > (await this.roleService.getUserPolicies(me.id)).webhookLimit) { + if (currentWebhooksCount >= (await this.roleService.getUserPolicies(me.id)).webhookLimit) { throw new ApiError(meta.errors.tooManyWebhooks); } -- cgit v1.2.3-freya From b50eb511b0cf6fb05d37c3370726f940c1438a99 Mon Sep 17 00:00:00 2001 From: yupix Date: Sat, 22 Jun 2024 14:52:27 +0900 Subject: refactor: api/*/update系の必須キーを最低限に (#13824) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: clips/updateの必須キーをclipIdのみに * refactor: admin/roles/update の必須キーをroleIdのみに * feat: pages/update の必須キーをpageIdのみに * refactor: gallery/posts/update の必須キーをpostidのみに * feat: misskey-jsの型を更新 * feat: i/webhooks/updateの必須キーをwebhookIdのみに * feat: admin/ad/updateの必須キーをidのみに * feat: misskey-jsの型を更新 * chore: update CHANGELOG.md * docs: update CHANGELOG.md * fix: secretが更新できなくなる場合がある Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com> * Update packages/backend/src/server/api/endpoints/gallery/posts/update.ts --------- Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 6 ++ .../src/server/api/endpoints/admin/ad/update.ts | 6 +- .../src/server/api/endpoints/admin/roles/update.ts | 14 ----- .../src/server/api/endpoints/clips/update.ts | 4 +- .../server/api/endpoints/gallery/posts/update.ts | 26 ++++---- .../src/server/api/endpoints/i/webhooks/update.ts | 6 +- .../src/server/api/endpoints/pages/update.ts | 23 +++---- packages/misskey-js/src/autogen/types.ts | 71 +++++++++++----------- 8 files changed, 71 insertions(+), 85 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/i') diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9f5f8000..c1af63ad23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) - Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006) - Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036) +- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに +- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに +- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに +- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに +- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに +- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに - Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) - Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正 diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index 62358457ff..4e3d731aca 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -40,7 +40,7 @@ export const paramDef = { startsAt: { type: 'integer' }, dayOfWeek: { type: 'integer' }, }, - required: ['id', 'memo', 'url', 'imageUrl', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'dayOfWeek'], + required: ['id'], } as const; @Injectable() @@ -63,8 +63,8 @@ export default class extends Endpoint { // eslint- ratio: ps.ratio, memo: ps.memo, imageUrl: ps.imageUrl, - expiresAt: new Date(ps.expiresAt), - startsAt: new Date(ps.startsAt), + expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined, + startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined, dayOfWeek: ps.dayOfWeek, }); diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index 5242e0be2f..465ad7aaaf 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -6,7 +6,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { RolesRepository } from '@/models/_.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { RoleService } from '@/core/RoleService.js'; @@ -50,19 +49,6 @@ export const paramDef = { }, required: [ 'roleId', - 'name', - 'description', - 'color', - 'iconUrl', - 'target', - 'condFormula', - 'isPublic', - 'isModerator', - 'isAdministrator', - 'asBadge', - 'canEditMembersByModerator', - 'displayOrder', - 'policies', ], } as const; diff --git a/packages/backend/src/server/api/endpoints/clips/update.ts b/packages/backend/src/server/api/endpoints/clips/update.ts index 3b44ba81b3..603a3ccf3d 100644 --- a/packages/backend/src/server/api/endpoints/clips/update.ts +++ b/packages/backend/src/server/api/endpoints/clips/update.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipService } from '@/core/ClipService.js'; @@ -41,7 +41,7 @@ export const paramDef = { isPublic: { type: 'boolean' }, description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 }, }, - required: ['clipId', 'name'], + required: ['clipId'], } as const; @Injectable() diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts index 2f977784ec..5243ee9603 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/update.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/update.ts @@ -47,7 +47,7 @@ export const paramDef = { } }, isSensitive: { type: 'boolean', default: false }, }, - required: ['postId', 'title', 'fileIds'], + required: ['postId'], } as const; @Injectable() @@ -62,15 +62,19 @@ export default class extends Endpoint { // eslint- private galleryPostEntityService: GalleryPostEntityService, ) { super(meta, paramDef, async (ps, me) => { - const files = (await Promise.all(ps.fileIds.map(fileId => - this.driveFilesRepository.findOneBy({ - id: fileId, - userId: me.id, - }), - ))).filter(x => x != null); - - if (files.length === 0) { - throw new Error(); + let files: Array | undefined; + + if (ps.fileIds) { + files = (await Promise.all(ps.fileIds.map(fileId => + this.driveFilesRepository.findOneBy({ + id: fileId, + userId: me.id, + }), + ))).filter(x => x != null); + + if (files.length === 0) { + throw new Error(); + } } await this.galleryPostsRepository.update({ @@ -81,7 +85,7 @@ export default class extends Endpoint { // eslint- title: ps.title, description: ps.description, isSensitive: ps.isSensitive, - fileIds: files.map(file => file.id), + fileIds: files ? files.map(file => file.id) : undefined, }); const post = await this.galleryPostsRepository.findOneByOrFail({ id: ps.postId }); diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts index 6e380d76f8..07a25bd82a 100644 --- a/packages/backend/src/server/api/endpoints/i/webhooks/update.ts +++ b/packages/backend/src/server/api/endpoints/i/webhooks/update.ts @@ -34,13 +34,13 @@ export const paramDef = { webhookId: { type: 'string', format: 'misskey:id' }, name: { type: 'string', minLength: 1, maxLength: 100 }, url: { type: 'string', minLength: 1, maxLength: 1024 }, - secret: { type: 'string', maxLength: 1024, default: '' }, + secret: { type: 'string', nullable: true, maxLength: 1024 }, on: { type: 'array', items: { type: 'string', enum: webhookEventTypes, } }, active: { type: 'boolean' }, }, - required: ['webhookId', 'name', 'url', 'on', 'active'], + required: ['webhookId'], } as const; // TODO: ロジックをサービスに切り出す @@ -66,7 +66,7 @@ export default class extends Endpoint { // eslint- await this.webhooksRepository.update(webhook.id, { name: ps.name, url: ps.url, - secret: ps.secret, + secret: ps.secret === null ? '' : ps.secret, on: ps.on, active: ps.active, }); diff --git a/packages/backend/src/server/api/endpoints/pages/update.ts b/packages/backend/src/server/api/endpoints/pages/update.ts index b8e5e70a25..f11bbbcb1a 100644 --- a/packages/backend/src/server/api/endpoints/pages/update.ts +++ b/packages/backend/src/server/api/endpoints/pages/update.ts @@ -70,7 +70,7 @@ export const paramDef = { alignCenter: { type: 'boolean' }, hideTitleWhenPinned: { type: 'boolean' }, }, - required: ['pageId', 'title', 'name', 'content', 'variables', 'script'], + required: ['pageId'], } as const; @Injectable() @@ -91,9 +91,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.accessDenied); } - let eyeCatchingImage = null; if (ps.eyeCatchingImageId != null) { - eyeCatchingImage = await this.driveFilesRepository.findOneBy({ + const eyeCatchingImage = await this.driveFilesRepository.findOneBy({ id: ps.eyeCatchingImageId, userId: me.id, }); @@ -116,23 +115,15 @@ export default class extends Endpoint { // eslint- await this.pagesRepository.update(page.id, { updatedAt: new Date(), title: ps.title, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - name: ps.name === undefined ? page.name : ps.name, + name: ps.name, summary: ps.summary === undefined ? page.summary : ps.summary, content: ps.content, variables: ps.variables, script: ps.script, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - font: ps.font === undefined ? page.font : ps.font, - eyeCatchingImageId: ps.eyeCatchingImageId === null - ? null - : ps.eyeCatchingImageId === undefined - ? page.eyeCatchingImageId - : eyeCatchingImage!.id, + alignCenter: ps.alignCenter, + hideTitleWhenPinned: ps.hideTitleWhenPinned, + font: ps.font, + eyeCatchingImageId: ps.eyeCatchingImageId, }); }); } diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index bdcc1dfd77..72aca4dee2 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5881,15 +5881,15 @@ export type operations = { 'application/json': { /** Format: misskey:id */ id: string; - memo: string; - url: string; - imageUrl: string; - place: string; - priority: string; - ratio: number; - expiresAt: number; - startsAt: number; - dayOfWeek: number; + memo?: string; + url?: string; + imageUrl?: string; + place?: string; + priority?: string; + ratio?: number; + expiresAt?: number; + startsAt?: number; + dayOfWeek?: number; }; }; }; @@ -9744,21 +9744,21 @@ export type operations = { 'application/json': { /** Format: misskey:id */ roleId: string; - name: string; - description: string; - color: string | null; - iconUrl: string | null; + name?: string; + description?: string; + color?: string | null; + iconUrl?: string | null; /** @enum {string} */ - target: 'manual' | 'conditional'; - condFormula: Record; - isPublic: boolean; - isModerator: boolean; - isAdministrator: boolean; + target?: 'manual' | 'conditional'; + condFormula?: Record; + isPublic?: boolean; + isModerator?: boolean; + isAdministrator?: boolean; isExplorable?: boolean; - asBadge: boolean; - canEditMembersByModerator: boolean; - displayOrder: number; - policies: Record; + asBadge?: boolean; + canEditMembersByModerator?: boolean; + displayOrder?: number; + policies?: Record; }; }; }; @@ -13400,7 +13400,7 @@ export type operations = { 'application/json': { /** Format: misskey:id */ clipId: string; - name: string; + name?: string; isPublic?: boolean; description?: string | null; }; @@ -16247,9 +16247,9 @@ export type operations = { 'application/json': { /** Format: misskey:id */ postId: string; - title: string; + title?: string; description?: string | null; - fileIds: string[]; + fileIds?: string[]; /** @default false */ isSensitive?: boolean; }; @@ -20030,12 +20030,11 @@ export type operations = { 'application/json': { /** Format: misskey:id */ webhookId: string; - name: string; - url: string; - /** @default */ - secret?: string; - on: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; - active: boolean; + name?: string; + url?: string; + secret?: string | null; + on?: ('mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction')[]; + active?: boolean; }; }; }; @@ -23404,16 +23403,16 @@ export type operations = { 'application/json': { /** Format: misskey:id */ pageId: string; - title: string; - name: string; + title?: string; + name?: string; summary?: string | null; - content: { + content?: { [key: string]: unknown; }[]; - variables: { + variables?: { [key: string]: unknown; }[]; - script: string; + script?: string; /** Format: misskey:id */ eyeCatchingImageId?: string | null; /** @enum {string} */ -- cgit v1.2.3-freya From 9ef6c4716ca25d8adeb5344c82a433370a086f88 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 7 Jul 2024 14:19:00 +0900 Subject: fix(backend): 名前を空白文字列だけにできる問題を修正 (#14119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): 名前を空白文字列だけにできる問題を修正 * Update Changelog * fix test * Unicodeを含める * fix * ユーザー名がUnicode制御文字とスペースのみで構成される場合はnullに * Revert "ユーザー名がUnicode制御文字とスペースのみで構成される場合はnullに" This reverts commit 6c752a69c0d3649072e7e4ed30025183bceb48f9. * [ci skip] changelog typo --- CHANGELOG.md | 2 ++ packages/backend/src/server/api/endpoints/i/update.ts | 9 ++++++++- packages/backend/test/e2e/endpoints.ts | 13 +++++++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/i') diff --git a/CHANGELOG.md b/CHANGELOG.md index 23de5957fd..f62f966451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ - Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正 - Fix: 空文字列のリアクションはフォールバックされるように - Fix: リノートにリアクションできないように +- Fix: ユーザー名の前後に空白文字列がある場合は省略するように +- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正 ### Misskey.js - Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index a8e702f328..b39b52bc41 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -257,7 +257,14 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (ps.name !== undefined) updates.name = ps.name; + if (ps.name !== undefined) { + if (ps.name === null) { + updates.name = null; + } else { + const trimmedName = ps.name.trim(); + updates.name = trimmedName === '' ? null : trimmedName; + } + } if (ps.description !== undefined) profileUpdates.description = ps.description; if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.location !== undefined) profileUpdates.location = ps.location; diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts index d5583ea8bb..2b2699ecd9 100644 --- a/packages/backend/test/e2e/endpoints.ts +++ b/packages/backend/test/e2e/endpoints.ts @@ -117,12 +117,21 @@ describe('Endpoints', () => { assert.strictEqual(res.body.birthday, myBirthday); }); - test('名前を空白にできる', async () => { + test('名前を空白のみにした場合nullになる', async () => { const res = await api('i/update', { name: ' ', }, alice); assert.strictEqual(res.status, 200); - assert.strictEqual(res.body.name, ' '); + assert.strictEqual(res.body.name, null); + }); + + test('名前の前後に空白(ホワイトスペース)を入れてもトリムされる', async () => { + const res = await api('i/update', { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#white_space + name: ' あ い う \u0009\u000b\u000c\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff', + }, alice); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.name, 'あ い う'); }); test('誕生日の設定を削除できる', async () => { -- cgit v1.2.3-freya From 7afa593d114335368d9031b6e1d5b1edbc4ea9c9 Mon Sep 17 00:00:00 2001 From: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com> Date: Sun, 14 Jul 2024 09:31:05 +0900 Subject: Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に (#14078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement role policy "canUpdateBioMedia" * docs(changelog): update changelog * docs(changelog): update changelog * chore: regenerate misskey-js type definitions * chore: Apply suggestion from code review Co-authored-by: anatawa12 * chore: fix unnecessarily strict inequality check * chore: policies should be gotten only once --------- Co-authored-by: anatawa12 --- CHANGELOG.md | 2 ++ locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + packages/backend/src/core/RoleService.ts | 3 +++ .../src/core/activitypub/models/ApPersonService.ts | 8 ++++++++ packages/backend/src/models/json-schema/role.ts | 4 ++++ .../backend/src/server/api/endpoints/i/update.ts | 23 ++++++++++++++++------ packages/frontend/src/const.ts | 1 + packages/frontend/src/pages/admin/roles.editor.vue | 20 +++++++++++++++++++ packages/frontend/src/pages/admin/roles.vue | 8 ++++++++ packages/misskey-js/src/autogen/types.ts | 1 + 11 files changed, 69 insertions(+), 6 deletions(-) (limited to 'packages/backend/src/server/api/endpoints/i') diff --git a/CHANGELOG.md b/CHANGELOG.md index bd2a9e32e5..bcc2aa29c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### General - Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705 +- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に + - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます - Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正 - Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題 - Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index 5089f7802e..c2f8e944dd 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -6594,6 +6594,10 @@ export interface Locale extends ILocale { * ファイルにNSFWを常に付与 */ "alwaysMarkNsfw": string; + /** + * アイコンとバナーの更新を許可 + */ + "canUpdateBioMedia": string; /** * ノートのピン留めの最大数 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a03d792140..8d117e6dc8 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1705,6 +1705,7 @@ _role: canManageAvatarDecorations: "アバターデコレーションの管理" driveCapacity: "ドライブ容量" alwaysMarkNsfw: "ファイルにNSFWを常に付与" + canUpdateBioMedia: "アイコンとバナーの更新を許可" pinMax: "ノートのピン留めの最大数" antennaMax: "アンテナの作成可能数" wordMuteMax: "ワードミュートの最大文字数" diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index e2ebecb99f..94026fd503 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -47,6 +47,7 @@ export type RolePolicies = { canHideAds: boolean; driveCapacityMb: number; alwaysMarkNsfw: boolean; + canUpdateBioMedia: boolean; pinLimit: number; antennaLimit: number; wordMuteLimit: number; @@ -75,6 +76,7 @@ export const DEFAULT_POLICIES: RolePolicies = { canHideAds: false, driveCapacityMb: 100, alwaysMarkNsfw: false, + canUpdateBioMedia: true, pinLimit: 5, antennaLimit: 5, wordMuteLimit: 200, @@ -376,6 +378,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)), + canUpdateBioMedia: calc('canUpdateBioMedia', vs => vs.some(v => v === true)), pinLimit: calc('pinLimit', vs => Math.max(...vs)), antennaLimit: calc('antennaLimit', vs => Math.max(...vs)), wordMuteLimit: calc('wordMuteLimit', vs => Math.max(...vs)), diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 398c8695d2..457205e023 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -34,6 +34,7 @@ import { StatusError } from '@/misc/status-error.js'; import type { UtilityService } from '@/core/UtilityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js'; @@ -100,6 +101,8 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, + + private roleService: RoleService, ) { } @@ -238,6 +241,11 @@ export class ApPersonService implements OnModuleInit { return this.apImageService.resolveImage(user, img).catch(() => null); })); + if (((avatar != null && avatar.id != null) || (banner != null && banner.id != null)) + && !(await this.roleService.getUserPolicies(user.id)).canUpdateBioMedia) { + return {}; + } + /* we don't want to return nulls on errors! if the database fields are already null, nothing changes; if the database has old diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index d9987a70c3..7366f05356 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -228,6 +228,10 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, + canUpdateBioMedia: { + type: 'boolean', + optional: false, nullable: false, + }, pinLimit: { type: 'integer', optional: false, nullable: false, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b39b52bc41..a1e2fa5e4c 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -25,7 +25,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { HashtagService } from '@/core/HashtagService.js'; import { DI } from '@/di-symbols.js'; -import { RoleService } from '@/core/RoleService.js'; +import { RolePolicies, RoleService } from '@/core/RoleService.js'; import { CacheService } from '@/core/CacheService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; @@ -256,6 +256,7 @@ export default class extends Endpoint { // eslint- const profileUpdates = {} as Partial; const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); + let policies: RolePolicies | null = null; if (ps.name !== undefined) { if (ps.name === null) { @@ -296,14 +297,16 @@ export default class extends Endpoint { // eslint- } if (ps.mutedWords !== undefined) { - checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + policies ??= await this.roleService.getUserPolicies(user.id); + checkMuteWordCount(ps.mutedWords, policies.wordMuteLimit); validateMuteWordRegex(ps.mutedWords); profileUpdates.mutedWords = ps.mutedWords; profileUpdates.enableWordMute = ps.mutedWords.length > 0; } if (ps.hardMutedWords !== undefined) { - checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit); + policies ??= await this.roleService.getUserPolicies(user.id); + checkMuteWordCount(ps.hardMutedWords, policies.wordMuteLimit); validateMuteWordRegex(ps.hardMutedWords); profileUpdates.hardMutedWords = ps.hardMutedWords; } @@ -322,13 +325,17 @@ export default class extends Endpoint { // eslint- if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.alwaysMarkNsfw === 'boolean') { - if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); + policies ??= await this.roleService.getUserPolicies(user.id); + if (policies.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; } if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive; if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; if (ps.avatarId) { + policies ??= await this.roleService.getUserPolicies(user.id); + if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole); + const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId }); if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); @@ -344,6 +351,9 @@ export default class extends Endpoint { // eslint- } if (ps.bannerId) { + policies ??= await this.roleService.getUserPolicies(user.id); + if (!policies.canUpdateBioMedia) throw new ApiError(meta.errors.restrictedByRole); + const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId }); if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); @@ -359,14 +369,15 @@ export default class extends Endpoint { // eslint- } if (ps.avatarDecorations) { + policies ??= await this.roleService.getUserPolicies(user.id); const decorations = await this.avatarDecorationService.getAll(true); - const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); + const myRoles = await this.roleService.getUserRoles(user.id); const allRoles = await this.roleService.getRoles(); const decorationIds = decorations .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .map(d => d.id); - if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); + if (ps.avatarDecorations.length > policies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ id: d.id, diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 9e41926a97..e135bc69a0 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -87,6 +87,7 @@ export const ROLE_POLICIES = [ 'canHideAds', 'driveCapacityMb', 'alwaysMarkNsfw', + 'canUpdateBioMedia', 'pinLimit', 'antennaLimit', 'wordMuteLimit', diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index eb8a59b34f..3e948abdf1 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -378,6 +378,26 @@ SPDX-License-Identifier: AGPL-3.0-only + + + +
+ + + + + + + + + +
+
+