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: _options:
gtlAvailable: "글로벌 타임라인 보이기" gtlAvailable: "글로벌 타임라인 보이기"
ltlAvailable: "로컬 타임라인 보이기" ltlAvailable: "로컬 타임라인 보이기"
canPublicNote: "공개 노트 허용" postVisibilityLimit: "사용할 수 있는 노트 공개 범위"
canInitiateConversation: "멘션, 답글, 인용 허용" canInitiateConversation: "멘션, 답글, 인용 허용"
mentionLimit: "노트에 넣을 수 있는 멘션 수" mentionLimit: "노트에 넣을 수 있는 멘션 수"
canCreateContent: "컨텐츠 생성 허용" 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.'); 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) {
const sensitiveWords = meta.sensitiveWords; if (policies.postVisibilityLimit < 3) {
if (this.utilityService.isKeyWordIncluded(data.cw ?? this.utilityService.concatNoteContentsForKeyWordCheck({ text: data.text, pollChoices: data.poll?.choices }), sensitiveWords)) { data.visibility = ['specified', 'followers', 'home'][policies.postVisibilityLimit];
data.visibility = 'home'; this.logger.warn(`Visibility changed to ${data.visibility} because of role policies`, {
this.logger.warn('Visibility changed to home because sensitive words are included', { userId: user.id, note: data }); userId: user.id,
} else if (!policies.canPublicNote) { note: data,
data.visibility = 'home'; });
}
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 = { 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)),

View file

@ -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);
} }

View file

@ -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: {

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 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)) {

View file

@ -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' });

View file

@ -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' });

View file

@ -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',

View file

@ -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>

View file

@ -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>