wip: postVisibilityLimit
This commit is contained in:
parent
407abe42cf
commit
cfcbcd8379
11 changed files with 80 additions and 52 deletions
|
@ -1848,7 +1848,7 @@ _role:
|
||||||
_options:
|
_options:
|
||||||
gtlAvailable: "글로벌 타임라인 보이기"
|
gtlAvailable: "글로벌 타임라인 보이기"
|
||||||
ltlAvailable: "로컬 타임라인 보이기"
|
ltlAvailable: "로컬 타임라인 보이기"
|
||||||
canPublicNote: "공개 노트 허용"
|
postVisibilityLimit: "사용할 수 있는 노트 공개 범위"
|
||||||
canInitiateConversation: "멘션, 답글, 인용 허용"
|
canInitiateConversation: "멘션, 답글, 인용 허용"
|
||||||
mentionLimit: "노트에 넣을 수 있는 멘션 수"
|
mentionLimit: "노트에 넣을 수 있는 멘션 수"
|
||||||
canCreateContent: "컨텐츠 생성 허용"
|
canCreateContent: "컨텐츠 생성 허용"
|
||||||
|
|
|
@ -261,13 +261,26 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
throw new IdentifiableError('5b1c2b67-50a6-4a8a-a59c-0ede40890de3', 'User has no permission to create content.');
|
throw new IdentifiableError('5b1c2b67-50a6-4a8a-a59c-0ede40890de3', 'User has no permission to create content.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.visibility === 'public' && data.channel == null) {
|
if (data.channel == null) {
|
||||||
|
if (policies.postVisibilityLimit < 3) {
|
||||||
|
data.visibility = ['specified', 'followers', 'home'][policies.postVisibilityLimit];
|
||||||
|
this.logger.warn(`Visibility changed to ${data.visibility} because of role policies`, {
|
||||||
|
userId: user.id,
|
||||||
|
note: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data.visibility === 'public') {
|
||||||
const sensitiveWords = meta.sensitiveWords;
|
const sensitiveWords = meta.sensitiveWords;
|
||||||
if (this.utilityService.isKeyWordIncluded(data.cw ?? this.utilityService.concatNoteContentsForKeyWordCheck({ text: data.text, pollChoices: data.poll?.choices }), sensitiveWords)) {
|
if (this.utilityService.isKeyWordIncluded(data.cw ?? this.utilityService.concatNoteContentsForKeyWordCheck({
|
||||||
data.visibility = 'home';
|
text: data.text,
|
||||||
this.logger.warn('Visibility changed to home because sensitive words are included', { userId: user.id, note: data });
|
pollChoices: data.poll?.choices,
|
||||||
} else if (!policies.canPublicNote) {
|
}), sensitiveWords)) {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
|
this.logger.warn('Visibility changed to home because sensitive words are included', {
|
||||||
|
userId: user.id,
|
||||||
|
note: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
|
||||||
export type RolePolicies = {
|
export type RolePolicies = {
|
||||||
gtlAvailable: boolean;
|
gtlAvailable: boolean;
|
||||||
ltlAvailable: boolean;
|
ltlAvailable: boolean;
|
||||||
canPublicNote: boolean;
|
postVisibilityLimit: number;
|
||||||
canInitiateConversation: boolean;
|
canInitiateConversation: boolean;
|
||||||
canCreateContent: boolean;
|
canCreateContent: boolean;
|
||||||
canUpdateContent: boolean;
|
canUpdateContent: boolean;
|
||||||
|
@ -79,7 +79,7 @@ export type RolePolicies = {
|
||||||
export const DEFAULT_POLICIES: RolePolicies = {
|
export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
gtlAvailable: true,
|
gtlAvailable: true,
|
||||||
ltlAvailable: true,
|
ltlAvailable: true,
|
||||||
canPublicNote: true,
|
postVisibilityLimit: 3,
|
||||||
canInitiateConversation: true,
|
canInitiateConversation: true,
|
||||||
canCreateContent: true,
|
canCreateContent: true,
|
||||||
canUpdateContent: true,
|
canUpdateContent: true,
|
||||||
|
@ -396,7 +396,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
return {
|
return {
|
||||||
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
||||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||||
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
postVisibilityLimit: calc('postVisibilityLimit', vs => Math.max(...vs)),
|
||||||
canInitiateConversation: calc('canInitiateConversation', vs => vs.some(v => v === true)),
|
canInitiateConversation: calc('canInitiateConversation', vs => vs.some(v => v === true)),
|
||||||
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
|
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
|
||||||
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
|
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
|
||||||
|
|
|
@ -543,8 +543,14 @@ export class UserEntityService implements OnModuleInit {
|
||||||
bannerUrl: user.bannerUrl,
|
bannerUrl: user.bannerUrl,
|
||||||
bannerBlurhash: user.bannerBlurhash,
|
bannerBlurhash: user.bannerBlurhash,
|
||||||
isLocked: user.isLocked,
|
isLocked: user.isLocked,
|
||||||
isSilenced: !policies?.canPublicNote,
|
isSilenced: (policies?.postVisibilityLimit ?? 0) < 3,
|
||||||
isLimited: !(policies?.canCreateContent && policies.canUpdateContent && policies.canDeleteContent && policies.canInitiateConversation),
|
isLimited: !(
|
||||||
|
policies?.canCreateContent &&
|
||||||
|
policies.canUpdateContent &&
|
||||||
|
policies.canDeleteContent &&
|
||||||
|
policies.canInitiateConversation &&
|
||||||
|
policies.canUseAccountRemoval
|
||||||
|
),
|
||||||
isSuspended: user.isSuspended,
|
isSuspended: user.isSuspended,
|
||||||
description: profile?.description,
|
description: profile?.description,
|
||||||
location: profile?.location,
|
location: profile?.location,
|
||||||
|
@ -630,9 +636,9 @@ export class UserEntityService implements OnModuleInit {
|
||||||
achievements: profile?.achievements,
|
achievements: profile?.achievements,
|
||||||
autoRemovalCondition: {
|
autoRemovalCondition: {
|
||||||
active: user.autoRemoval,
|
active: user.autoRemoval,
|
||||||
deleteAfter: autoRemovalCondition?.deleteAfter,
|
deleteAfter: autoRemovalCondition.deleteAfter,
|
||||||
noPiningNotes: autoRemovalCondition?.noPiningNotes,
|
noPiningNotes: autoRemovalCondition.noPiningNotes,
|
||||||
noSpecifiedNotes: autoRemovalCondition?.noSpecifiedNotes,
|
noSpecifiedNotes: autoRemovalCondition.noSpecifiedNotes,
|
||||||
},
|
},
|
||||||
loggedInDays: profile?.loggedInDates.length,
|
loggedInDays: profile?.loggedInDates.length,
|
||||||
policies: policies,
|
policies: policies,
|
||||||
|
@ -731,7 +737,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap?.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
|
return (await Promise.allSettled(_users.map(u => this.pack(u, me, { ...options, userProfile: profilesMap.get(u.id), userRelations: userRelations, userMemos: userMemos, pinNotes: pinNotes }))))
|
||||||
.filter(result => result.status === 'fulfilled')
|
.filter(result => result.status === 'fulfilled')
|
||||||
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
|
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,8 +176,8 @@ export const packedRolePoliciesSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
canPublicNote: {
|
postVisibilityLimit: {
|
||||||
type: 'boolean',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
canInitiateConversation: {
|
canInitiateConversation: {
|
||||||
|
|
|
@ -220,8 +220,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
const policies = await this.roleService.getUserPolicies(user.id);
|
const policies = await this.roleService.getUserPolicies(user.id);
|
||||||
const isModerator = await this.roleService.isModerator(user);
|
const isModerator = await this.roleService.isModerator(user);
|
||||||
const isLimited = !(policies.canCreateContent && policies.canUpdateContent && policies.canDeleteContent && policies.canInitiateConversation);
|
const isLimited = !(
|
||||||
const isSilenced = !policies.canPublicNote;
|
policies.canCreateContent &&
|
||||||
|
policies.canUpdateContent &&
|
||||||
|
policies.canDeleteContent &&
|
||||||
|
policies.canInitiateConversation &&
|
||||||
|
policies.canUseAccountRemoval
|
||||||
|
);
|
||||||
|
const isSilenced = policies.postVisibilityLimit < 3;
|
||||||
|
|
||||||
const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
|
const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||||
if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) {
|
if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) {
|
||||||
|
|
|
@ -94,7 +94,7 @@ describe('アンテナ', () => {
|
||||||
await api('i/update', { isLocked: true }, userLocking);
|
await api('i/update', { isLocked: true }, userLocking);
|
||||||
userSilenced = await signup({ username: 'userSilenced' });
|
userSilenced = await signup({ username: 'userSilenced' });
|
||||||
await post(userSilenced, { text: 'test' });
|
await post(userSilenced, { text: 'test' });
|
||||||
const roleSilenced = await role(root, {}, { canPublicNote: { priority: 0, useDefault: false, value: false } });
|
const roleSilenced = await role(root, {}, { postVisibilityLimit: { priority: 0, useDefault: false, value: 2 } });
|
||||||
await api('admin/roles/assign', { userId: userSilenced.id, roleId: roleSilenced.id }, root);
|
await api('admin/roles/assign', { userId: userSilenced.id, roleId: roleSilenced.id }, root);
|
||||||
userSuspended = await signup({ username: 'userSuspended' });
|
userSuspended = await signup({ username: 'userSuspended' });
|
||||||
await post(userSuspended, { text: 'test' });
|
await post(userSuspended, { text: 'test' });
|
||||||
|
|
|
@ -238,7 +238,7 @@ describe('ユーザー', () => {
|
||||||
await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root);
|
await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root);
|
||||||
userSilenced = await signup({ username: 'userSilenced' });
|
userSilenced = await signup({ username: 'userSilenced' });
|
||||||
await post(userSilenced, { text: 'test' });
|
await post(userSilenced, { text: 'test' });
|
||||||
roleSilenced = await role(root, {}, { canPublicNote: { priority: 0, useDefault: false, value: false } });
|
roleSilenced = await role(root, {}, { postVisibilityLimit: { priority: 0, useDefault: false, value: 2 } });
|
||||||
await api('admin/roles/assign', { userId: userSilenced.id, roleId: roleSilenced.id }, root);
|
await api('admin/roles/assign', { userId: userSilenced.id, roleId: roleSilenced.id }, root);
|
||||||
userSuspended = await signup({ username: 'userSuspended' });
|
userSuspended = await signup({ username: 'userSuspended' });
|
||||||
await post(userSuspended, { text: 'test' });
|
await post(userSuspended, { text: 'test' });
|
||||||
|
|
|
@ -74,7 +74,7 @@ export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
|
||||||
export const ROLE_POLICIES = [
|
export const ROLE_POLICIES = [
|
||||||
'gtlAvailable',
|
'gtlAvailable',
|
||||||
'ltlAvailable',
|
'ltlAvailable',
|
||||||
'canPublicNote',
|
'postVisibilityLimit',
|
||||||
'canInitiateConversation',
|
'canInitiateConversation',
|
||||||
'canCreateContent',
|
'canCreateContent',
|
||||||
'canUpdateContent',
|
'canUpdateContent',
|
||||||
|
|
|
@ -105,6 +105,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.postVisibilityLimit, 'postVisibilityLimit'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.postVisibilityLimit }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.postVisibilityLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ `${Math.floor(role.policies.rateLimitFactor.value * 100)}%` }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.postVisibilityLimit)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.postVisibilityLimit.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkRange :modelValue="role.policies.postVisibilityLimit.value * 100" :min="0" :max="3" :step="1" :textConverter="(v) => `${v}%`" @update:modelValue="v => role.policies.rateLimitFactor.value = (v / 100)">
|
||||||
|
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
|
||||||
|
<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
|
||||||
|
</MkRange>
|
||||||
|
<MkRange v-model="role.policies.rateLimitFactor.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 : ''">
|
||||||
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
-->
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])">
|
||||||
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
|
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
@ -145,26 +168,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canPublicNote, 'canPublicNote'])">
|
|
||||||
<template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
|
|
||||||
<template #suffix>
|
|
||||||
<span v-if="role.policies.canPublicNote.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
|
||||||
<span v-else>{{ role.policies.canPublicNote.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
|
||||||
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canPublicNote)"></i></span>
|
|
||||||
</template>
|
|
||||||
<div class="_gaps">
|
|
||||||
<MkSwitch v-model="role.policies.canPublicNote.useDefault" :readonly="readonly">
|
|
||||||
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
|
||||||
</MkSwitch>
|
|
||||||
<MkSwitch v-model="role.policies.canPublicNote.value" :disabled="role.policies.canPublicNote.useDefault" :readonly="readonly">
|
|
||||||
<template #label>{{ i18n.ts.enable }}</template>
|
|
||||||
</MkSwitch>
|
|
||||||
<MkRange v-model="role.policies.canPublicNote.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 : ''">
|
|
||||||
<template #label>{{ i18n.ts._role.priority }}</template>
|
|
||||||
</MkRange>
|
|
||||||
</div>
|
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInitiateConversation, 'canInitiateConversation'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInitiateConversation, 'canInitiateConversation'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
|
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
|
|
@ -24,6 +24,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkRange>
|
</MkRange>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.postVisibilityLimit, 'postVisibilityLimit'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.postVisibilityLimit }}</template>
|
||||||
|
<template #suffix>{{ [i18n.ts._visibility.specified, i18n.ts._visibility.followers, i18n.ts._visibility.home, i18n.ts._visibility.public][policies.postVisibilityLimit] }}</template>
|
||||||
|
<MkRange :modelValue="policies.postVisibilityLimit" :min="0" :max="3" :step="1" :textConverter="(v) => [i18n.ts._visibility.specified, i18n.ts._visibility.followers, i18n.ts._visibility.home, i18n.ts._visibility.public][v]" @update:modelValue="v => policies.rateLimitFactor = v">
|
||||||
|
<template #caption>{{ i18n.ts._role._options.descriptionOfPostVisibilityLimit }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.gtlAvailable, 'gtlAvailable'])">
|
||||||
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
|
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
|
||||||
<template #suffix>{{ policies.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
<template #suffix>{{ policies.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
@ -40,14 +48,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canPublicNote, 'canPublicNote'])">
|
|
||||||
<template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
|
|
||||||
<template #suffix>{{ policies.canPublicNote ? i18n.ts.yes : i18n.ts.no }}</template>
|
|
||||||
<MkSwitch v-model="policies.canPublicNote">
|
|
||||||
<template #label>{{ i18n.ts.enable }}</template>
|
|
||||||
</MkSwitch>
|
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInitiateConversation, 'canInitiateConversation'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInitiateConversation, 'canInitiateConversation'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
|
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
|
||||||
<template #suffix>{{ policies.canInitiateConversation ? i18n.ts.yes : i18n.ts.no }}</template>
|
<template #suffix>{{ policies.canInitiateConversation ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue