wip: postVisibilityLimit

This commit is contained in:
ASTRO:? 2024-12-28 14:36:49 +09:00
parent 407abe42cf
commit cfcbcd8379
No known key found for this signature in database
GPG key ID: 8947F3AF5B0B4BFE
11 changed files with 80 additions and 52 deletions

View file

@ -1848,7 +1848,7 @@ _role:
_options:
gtlAvailable: "글로벌 타임라인 보이기"
ltlAvailable: "로컬 타임라인 보이기"
canPublicNote: "공개 노트 허용"
postVisibilityLimit: "사용할 수 있는 노트 공개 범위"
canInitiateConversation: "멘션, 답글, 인용 허용"
mentionLimit: "노트에 넣을 수 있는 멘션 수"
canCreateContent: "컨텐츠 생성 허용"

View file

@ -261,13 +261,26 @@ export class NoteCreateService implements OnApplicationShutdown {
throw new IdentifiableError('5b1c2b67-50a6-4a8a-a59c-0ede40890de3', 'User has no permission to create content.');
}
if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = meta.sensitiveWords;
if (this.utilityService.isKeyWordIncluded(data.cw ?? this.utilityService.concatNoteContentsForKeyWordCheck({ text: data.text, pollChoices: data.poll?.choices }), sensitiveWords)) {
data.visibility = 'home';
this.logger.warn('Visibility changed to home because sensitive words are included', { userId: user.id, note: data });
} else if (!policies.canPublicNote) {
data.visibility = 'home';
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;
if (this.utilityService.isKeyWordIncluded(data.cw ?? this.utilityService.concatNoteContentsForKeyWordCheck({
text: data.text,
pollChoices: data.poll?.choices,
}), sensitiveWords)) {
data.visibility = 'home';
this.logger.warn('Visibility changed to home because sensitive words are included', {
userId: user.id,
note: data,
});
}
}
}

View file

@ -35,7 +35,7 @@ import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;
canPublicNote: boolean;
postVisibilityLimit: number;
canInitiateConversation: boolean;
canCreateContent: boolean;
canUpdateContent: boolean;
@ -79,7 +79,7 @@ export type RolePolicies = {
export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true,
ltlAvailable: true,
canPublicNote: true,
postVisibilityLimit: 3,
canInitiateConversation: true,
canCreateContent: true,
canUpdateContent: true,
@ -396,7 +396,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
return {
gtlAvailable: calc('gtlAvailable', 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)),
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),

View file

@ -543,8 +543,14 @@ export class UserEntityService implements OnModuleInit {
bannerUrl: user.bannerUrl,
bannerBlurhash: user.bannerBlurhash,
isLocked: user.isLocked,
isSilenced: !policies?.canPublicNote,
isLimited: !(policies?.canCreateContent && policies.canUpdateContent && policies.canDeleteContent && policies.canInitiateConversation),
isSilenced: (policies?.postVisibilityLimit ?? 0) < 3,
isLimited: !(
policies?.canCreateContent &&
policies.canUpdateContent &&
policies.canDeleteContent &&
policies.canInitiateConversation &&
policies.canUseAccountRemoval
),
isSuspended: user.isSuspended,
description: profile?.description,
location: profile?.location,
@ -630,9 +636,9 @@ export class UserEntityService implements OnModuleInit {
achievements: profile?.achievements,
autoRemovalCondition: {
active: user.autoRemoval,
deleteAfter: autoRemovalCondition?.deleteAfter,
noPiningNotes: autoRemovalCondition?.noPiningNotes,
noSpecifiedNotes: autoRemovalCondition?.noSpecifiedNotes,
deleteAfter: autoRemovalCondition.deleteAfter,
noPiningNotes: autoRemovalCondition.noPiningNotes,
noSpecifiedNotes: autoRemovalCondition.noSpecifiedNotes,
},
loggedInDays: profile?.loggedInDates.length,
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')
.map(result => (result as PromiseFulfilledResult<Packed<S>>).value);
}

View file

@ -176,8 +176,8 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
canPublicNote: {
type: 'boolean',
postVisibilityLimit: {
type: 'integer',
optional: false, nullable: false,
},
canInitiateConversation: {

View file

@ -220,8 +220,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const policies = await this.roleService.getUserPolicies(user.id);
const isModerator = await this.roleService.isModerator(user);
const isLimited = !(policies.canCreateContent && policies.canUpdateContent && policies.canDeleteContent && policies.canInitiateConversation);
const isSilenced = !policies.canPublicNote;
const isLimited = !(
policies.canCreateContent &&
policies.canUpdateContent &&
policies.canDeleteContent &&
policies.canInitiateConversation &&
policies.canUseAccountRemoval
);
const isSilenced = policies.postVisibilityLimit < 3;
const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) {

View file

@ -94,7 +94,7 @@ describe('アンテナ', () => {
await api('i/update', { isLocked: true }, userLocking);
userSilenced = await signup({ username: 'userSilenced' });
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);
userSuspended = await signup({ username: 'userSuspended' });
await post(userSuspended, { text: 'test' });

View file

@ -238,7 +238,7 @@ describe('ユーザー', () => {
await api('admin/roles/assign', { userId: userRoleBadge.id, roleId: roleBadge.id }, root);
userSilenced = await signup({ username: 'userSilenced' });
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);
userSuspended = await signup({ username: 'userSuspended' });
await post(userSuspended, { text: 'test' });

View file

@ -74,7 +74,7 @@ export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
export const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
'postVisibilityLimit',
'canInitiateConversation',
'canCreateContent',
'canUpdateContent',

View file

@ -105,6 +105,29 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</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'])">
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
<template #suffix>
@ -145,26 +168,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</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'])">
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
<template #suffix>

View file

@ -24,6 +24,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRange>
</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'])">
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
<template #suffix>{{ policies.gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
@ -40,14 +48,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</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'])">
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
<template #suffix>{{ policies.canInitiateConversation ? i18n.ts.yes : i18n.ts.no }}</template>