Skip to content

Commit 3753915

Browse files
committed
feat(role): リモートのアイコンとバナーの変更をロールで制限できるように
1 parent 05b1b32 commit 3753915

File tree

10 files changed

+94
-5
lines changed

10 files changed

+94
-5
lines changed

locales/en-US.yml

+2
Original file line numberDiff line numberDiff line change
@@ -1620,6 +1620,8 @@ _role:
16201620
canCreateContent: "Can create contents"
16211621
canUpdateContent: "Can edit contents"
16221622
canDeleteContent: "Can delete contents"
1623+
canUpdateAvatar: "Can change avatar"
1624+
canUpdateBanner: "Can change banner"
16231625
canInvite: "Can create instance invite codes"
16241626
inviteLimit: "Invite limit"
16251627
inviteLimitCycle: "Invite limit cooldown"

locales/index.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -6456,6 +6456,14 @@ export interface Locale extends ILocale {
64566456
* コンテンツの削除
64576457
*/
64586458
"canDeleteContent": string;
6459+
/**
6460+
* アイコンの変更
6461+
*/
6462+
"canUpdateAvatar": string;
6463+
/**
6464+
* バナーの変更
6465+
*/
6466+
"canUpdateBanner": string;
64596467
/**
64606468
* サーバー招待コードの発行
64616469
*/

locales/ja-JP.yml

+2
Original file line numberDiff line numberDiff line change
@@ -1671,6 +1671,8 @@ _role:
16711671
canCreateContent: "コンテンツの作成"
16721672
canUpdateContent: "コンテンツの編集"
16731673
canDeleteContent: "コンテンツの削除"
1674+
canUpdateAvatar: "アイコンの変更"
1675+
canUpdateBanner: "バナーの変更"
16741676
canInvite: "サーバー招待コードの発行"
16751677
inviteLimit: "招待コードの作成可能数"
16761678
inviteLimitCycle: "招待コードの発行間隔"

locales/ko-KR.yml

+5
Original file line numberDiff line numberDiff line change
@@ -1650,6 +1650,11 @@ _role:
16501650
gtlAvailable: "글로벌 타임라인 보이기"
16511651
ltlAvailable: "로컬 타임라인 보이기"
16521652
canPublicNote: "공개 노트 허용"
1653+
canCreateContent: "컨텐츠 생성 허용"
1654+
canUpdateContent: "컨텐츠 수정 허용"
1655+
canDeleteContent: "컨텐츠 삭제 허용"
1656+
canUpdateAvatar: "아바타 변경 허용"
1657+
canUpdateBanner: "배너 변경 허용"
16531658
canInvite: "서버 초대 코드 발행"
16541659
inviteLimit: "초대 한도"
16551660
inviteLimitCycle: "초대 발급 간격"

packages/backend/src/core/RoleService.ts

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export type RolePolicies = {
3939
canCreateContent: boolean;
4040
canUpdateContent: boolean;
4141
canDeleteContent: boolean;
42+
canUpdateAvatar: boolean;
43+
canUpdateBanner: boolean;
4244
canInvite: boolean;
4345
inviteLimit: number;
4446
inviteLimitCycle: number;
@@ -70,6 +72,8 @@ export const DEFAULT_POLICIES: RolePolicies = {
7072
canCreateContent: true,
7173
canUpdateContent: true,
7274
canDeleteContent: true,
75+
canUpdateAvatar: true,
76+
canUpdateBanner: true,
7377
canInvite: false,
7478
inviteLimit: 0,
7579
inviteLimitCycle: 60 * 24 * 7,
@@ -337,6 +341,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
337341
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
338342
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
339343
canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)),
344+
canUpdateAvatar: calc('canUpdateAvatar', vs => vs.some(v => v === true)),
345+
canUpdateBanner: calc('canUpdateBanner', vs => vs.some(v => v === true)),
340346
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
341347
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
342348
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),

packages/backend/src/core/activitypub/models/ApPersonService.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type Logger from '@/logger.js';
2020
import type { MiNote } from '@/models/Note.js';
2121
import type { IdService } from '@/core/IdService.js';
2222
import type { MfmService } from '@/core/MfmService.js';
23+
import type { RoleService } from '@/core/RoleService.js';
2324
import { toArray } from '@/misc/prelude/array.js';
2425
import type { GlobalEventService } from '@/core/GlobalEventService.js';
2526
import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -75,6 +76,7 @@ export class ApPersonService implements OnModuleInit {
7576
private instanceChart: InstanceChart;
7677
private apLoggerService: ApLoggerService;
7778
private accountMoveService: AccountMoveService;
79+
private roleService: RoleService;
7880
private logger: Logger;
7981

8082
constructor(
@@ -123,6 +125,7 @@ export class ApPersonService implements OnModuleInit {
123125
this.instanceChart = this.moduleRef.get('InstanceChart');
124126
this.apLoggerService = this.moduleRef.get('ApLoggerService');
125127
this.accountMoveService = this.moduleRef.get('AccountMoveService');
128+
this.roleService = this.moduleRef.get('RoleService');
126129
this.logger = this.apLoggerService.logger;
127130
}
128131

@@ -462,6 +465,8 @@ export class ApPersonService implements OnModuleInit {
462465
throw new Error('unexpected schema of person url: ' + url);
463466
}
464467

468+
const policy = await this.roleService.getUserPolicies(exist.id);
469+
465470
const updates = {
466471
lastFetchedAt: new Date(),
467472
inbox: person.inbox,
@@ -477,7 +482,7 @@ export class ApPersonService implements OnModuleInit {
477482
movedToUri: person.movedTo ?? null,
478483
alsoKnownAs: person.alsoKnownAs ?? null,
479484
isExplorable: person.discoverable,
480-
...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))),
485+
...((policy.canUpdateAvatar || policy.canUpdateBanner) ? await this.resolveAvatarAndBanner(exist, policy.canUpdateAvatar ? person.icon : exist.avatarUrl, policy.canUpdateBanner ? person.image : exist.bannerUrl).catch(() => ({})) : {}),
481486
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
482487

483488
const moving = ((): boolean => {

packages/backend/src/server/api/endpoints/i/update.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
233233

234234
const updates = {} as Partial<MiUser>;
235235
const profileUpdates = {} as Partial<MiUserProfile>;
236+
const policy = await this.roleService.getUserPolicies(user.id);
236237

237238
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
238239

@@ -245,7 +246,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
245246
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
246247
if (ps.mutedWords !== undefined) {
247248
const length = ps.mutedWords.length;
248-
if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {
249+
if (length > policy.wordMuteLimit) {
249250
throw new ApiError(meta.errors.tooManyMutedWords);
250251
}
251252

@@ -279,13 +280,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
279280
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
280281
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
281282
if (typeof ps.alwaysMarkNsfw === 'boolean') {
282-
if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
283+
if (policy.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole);
283284
profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
284285
}
285286
if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive;
286287
if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes;
287288

288289
if (ps.avatarId) {
290+
if (!policy.canUpdateAvatar) throw new ApiError(meta.errors.restrictedByRole);
289291
const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId });
290292

291293
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
@@ -301,6 +303,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
301303
}
302304

303305
if (ps.bannerId) {
306+
if (!policy.canUpdateBanner) throw new ApiError(meta.errors.restrictedByRole);
304307
const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId });
305308

306309
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
@@ -317,13 +320,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
317320

318321
if (ps.avatarDecorations) {
319322
const decorations = await this.avatarDecorationService.getAll(true);
320-
const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]);
323+
const myRoles = await this.roleService.getUserRoles(user.id);
321324
const allRoles = await this.roleService.getRoles();
322325
const decorationIds = decorations
323326
.filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id)))
324327
.map(d => d.id);
325328

326-
if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
329+
if (ps.avatarDecorations.length > policy.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole);
327330

328331
updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({
329332
id: d.id,

packages/frontend/src/const.ts

+2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ export const ROLE_POLICIES = [
7878
'canCreateContent',
7979
'canUpdateContent',
8080
'canDeleteContent',
81+
'canUpdateAvatar',
82+
'canUpdateBanner',
8183
'canInvite',
8284
'inviteLimit',
8385
'inviteLimitCycle',

packages/frontend/src/pages/admin/roles.editor.vue

+40
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,46 @@ SPDX-License-Identifier: AGPL-3.0-only
220220
</div>
221221
</MkFolder>
222222

223+
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateAvatar, 'canUpdateAvatar'])">
224+
<template #label>{{ i18n.ts._role._options.canUpdateAvatar }}</template>
225+
<template #suffix>
226+
<span v-if="role.policies.canUpdateAvatar.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
227+
<span v-else>{{ role.policies.canUpdateAvatar.value ? i18n.ts.yes : i18n.ts.no }}</span>
228+
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUpdateAvatar)"></i></span>
229+
</template>
230+
<div class="_gaps">
231+
<MkSwitch v-model="role.policies.canUpdateAvatar.useDefault" :readonly="readonly">
232+
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
233+
</MkSwitch>
234+
<MkSwitch v-model="role.policies.canUpdateAvatar.value" :disabled="role.policies.canUpdateAvatar.useDefault" :readonly="readonly">
235+
<template #label>{{ i18n.ts.enable }}</template>
236+
</MkSwitch>
237+
<MkRange v-model="role.policies.canUpdateAvatar.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
238+
<template #label>{{ i18n.ts._role.priority }}</template>
239+
</MkRange>
240+
</div>
241+
</MkFolder>
242+
243+
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBanner, 'canUpdateBanner'])">
244+
<template #label>{{ i18n.ts._role._options.canUpdateBanner }}</template>
245+
<template #suffix>
246+
<span v-if="role.policies.canUpdateBanner.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
247+
<span v-else>{{ role.policies.canUpdateBanner.value ? i18n.ts.yes : i18n.ts.no }}</span>
248+
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUpdateBanner)"></i></span>
249+
</template>
250+
<div class="_gaps">
251+
<MkSwitch v-model="role.policies.canUpdateBanner.useDefault" :readonly="readonly">
252+
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
253+
</MkSwitch>
254+
<MkSwitch v-model="role.policies.canUpdateBanner.value" :disabled="role.policies.canUpdateBanner.useDefault" :readonly="readonly">
255+
<template #label>{{ i18n.ts.enable }}</template>
256+
</MkSwitch>
257+
<MkRange v-model="role.policies.canUpdateBanner.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
258+
<template #label>{{ i18n.ts._role.priority }}</template>
259+
</MkRange>
260+
</div>
261+
</MkFolder>
262+
223263
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
224264
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
225265
<template #suffix>

packages/frontend/src/pages/admin/roles.vue

+16
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ SPDX-License-Identifier: AGPL-3.0-only
7272
</MkSwitch>
7373
</MkFolder>
7474

75+
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateAvatar, 'canUpdateAvatar'])">
76+
<template #label>{{ i18n.ts._role._options.canUpdateAvatar }}</template>
77+
<template #suffix>{{ policies.canUpdateAvatar ? i18n.ts.yes : i18n.ts.no }}</template>
78+
<MkSwitch v-model="policies.canUpdateAvatar">
79+
<template #label>{{ i18n.ts.enable }}</template>
80+
</MkSwitch>
81+
</MkFolder>
82+
83+
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateBanner, 'canUpdateBanner'])">
84+
<template #label>{{ i18n.ts._role._options.canUpdateBanner }}</template>
85+
<template #suffix>{{ policies.canUpdateBanner ? i18n.ts.yes : i18n.ts.no }}</template>
86+
<MkSwitch v-model="policies.canUpdateBanner">
87+
<template #label>{{ i18n.ts.enable }}</template>
88+
</MkSwitch>
89+
</MkFolder>
90+
7591
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
7692
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
7793
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>

0 commit comments

Comments
 (0)