diff --git a/locales/en-US.yml b/locales/en-US.yml index 9caac5046..8c437712f 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1620,6 +1620,8 @@ _role: canCreateContent: "Can create contents" canUpdateContent: "Can edit contents" canDeleteContent: "Can delete contents" + canUpdateAvatar: "Can change avatar" + canUpdateBanner: "Can change banner" canInvite: "Can create instance invite codes" inviteLimit: "Invite limit" inviteLimitCycle: "Invite limit cooldown" diff --git a/locales/index.d.ts b/locales/index.d.ts index c5a4849b0..77b9d3d84 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -6456,6 +6456,14 @@ export interface Locale extends ILocale { * コンテンツの削除 */ "canDeleteContent": string; + /** + * アイコンの変更 + */ + "canUpdateAvatar": string; + /** + * バナーの変更 + */ + "canUpdateBanner": string; /** * サーバー招待コードの発行 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7988f60ab..609d2a8fe 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1671,6 +1671,8 @@ _role: canCreateContent: "コンテンツの作成" canUpdateContent: "コンテンツの編集" canDeleteContent: "コンテンツの削除" + canUpdateAvatar: "アイコンの変更" + canUpdateBanner: "バナーの変更" canInvite: "サーバー招待コードの発行" inviteLimit: "招待コードの作成可能数" inviteLimitCycle: "招待コードの発行間隔" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index a1a7ee3ec..dccb7af30 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1650,6 +1650,11 @@ _role: gtlAvailable: "글로벌 타임라인 보이기" ltlAvailable: "로컬 타임라인 보이기" canPublicNote: "공개 노트 허용" + canCreateContent: "컨텐츠 생성 허용" + canUpdateContent: "컨텐츠 수정 허용" + canDeleteContent: "컨텐츠 삭제 허용" + canUpdateAvatar: "아바타 변경 허용" + canUpdateBanner: "배너 변경 허용" canInvite: "서버 초대 코드 발행" inviteLimit: "초대 한도" inviteLimitCycle: "초대 발급 간격" diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 1e1db2523..b0e2cfbfa 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -39,6 +39,8 @@ export type RolePolicies = { canCreateContent: boolean; canUpdateContent: boolean; canDeleteContent: boolean; + canUpdateAvatar: boolean; + canUpdateBanner: boolean; canInvite: boolean; inviteLimit: number; inviteLimitCycle: number; @@ -70,6 +72,8 @@ export const DEFAULT_POLICIES: RolePolicies = { canCreateContent: true, canUpdateContent: true, canDeleteContent: true, + canUpdateAvatar: true, + canUpdateBanner: true, canInvite: false, inviteLimit: 0, inviteLimitCycle: 60 * 24 * 7, @@ -337,6 +341,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)), canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)), canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)), + canUpdateAvatar: calc('canUpdateAvatar', vs => vs.some(v => v === true)), + canUpdateBanner: calc('canUpdateBanner', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), inviteLimitCycle: calc('inviteLimitCycle', 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 b1becbaba..772c79281 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -20,6 +20,7 @@ import type Logger from '@/logger.js'; import type { MiNote } from '@/models/Note.js'; import type { IdService } from '@/core/IdService.js'; import type { MfmService } from '@/core/MfmService.js'; +import type { RoleService } from '@/core/RoleService.js'; import { toArray } from '@/misc/prelude/array.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; @@ -75,6 +76,7 @@ export class ApPersonService implements OnModuleInit { private instanceChart: InstanceChart; private apLoggerService: ApLoggerService; private accountMoveService: AccountMoveService; + private roleService: RoleService; private logger: Logger; constructor( @@ -123,6 +125,7 @@ export class ApPersonService implements OnModuleInit { this.instanceChart = this.moduleRef.get('InstanceChart'); this.apLoggerService = this.moduleRef.get('ApLoggerService'); this.accountMoveService = this.moduleRef.get('AccountMoveService'); + this.roleService = this.moduleRef.get('RoleService'); this.logger = this.apLoggerService.logger; } @@ -462,6 +465,8 @@ export class ApPersonService implements OnModuleInit { throw new Error('unexpected schema of person url: ' + url); } + const policy = await this.roleService.getUserPolicies(exist.id); + const updates = { lastFetchedAt: new Date(), inbox: person.inbox, @@ -477,7 +482,7 @@ export class ApPersonService implements OnModuleInit { movedToUri: person.movedTo ?? null, alsoKnownAs: person.alsoKnownAs ?? null, isExplorable: person.discoverable, - ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))), + ...((policy.canUpdateAvatar || policy.canUpdateBanner) ? await this.resolveAvatarAndBanner(exist, policy.canUpdateAvatar ? person.icon : exist.avatarUrl, policy.canUpdateBanner ? person.image : exist.bannerUrl).catch(() => ({})) : {}), } as Partial & Pick; const moving = ((): boolean => { diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 1ee02bde3..d8ecd7842 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -233,6 +233,7 @@ export default class extends Endpoint { // eslint- const updates = {} as Partial; const profileUpdates = {} as Partial; + const policy = await this.roleService.getUserPolicies(user.id); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); @@ -245,7 +246,7 @@ export default class extends Endpoint { // eslint- if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility; if (ps.mutedWords !== undefined) { const length = ps.mutedWords.length; - if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) { + if (length > policy.wordMuteLimit) { throw new ApiError(meta.errors.tooManyMutedWords); } @@ -279,13 +280,14 @@ 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); + if (policy.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) { + if (!policy.canUpdateAvatar) 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); @@ -301,6 +303,7 @@ export default class extends Endpoint { // eslint- } if (ps.bannerId) { + if (!policy.canUpdateBanner) 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); @@ -317,13 +320,13 @@ export default class extends Endpoint { // eslint- if (ps.avatarDecorations) { 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 > policy.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 90d33da92..55f4dbc37 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -78,6 +78,8 @@ export const ROLE_POLICIES = [ 'canCreateContent', 'canUpdateContent', 'canDeleteContent', + 'canUpdateAvatar', + 'canUpdateBanner', 'canInvite', 'inviteLimit', 'inviteLimitCycle', diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index cd8a492f6..d9aa671bd 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -220,6 +220,46 @@ SPDX-License-Identifier: AGPL-3.0-only + + + +
+ + + + + + + + + +
+
+ + + + +
+ + + + + + + + + +
+
+