feat: ロールによるメンション、リプライ、引用の制限 (MisskeyIO#478)
This commit is contained in:
parent
ce98a86c89
commit
a9912534fe
|
@ -1664,6 +1664,7 @@ _role:
|
||||||
gtlAvailable: "Can view the global timeline"
|
gtlAvailable: "Can view the global timeline"
|
||||||
ltlAvailable: "Can view the local timeline"
|
ltlAvailable: "Can view the local timeline"
|
||||||
canPublicNote: "Can send public notes"
|
canPublicNote: "Can send public notes"
|
||||||
|
canInitiateConversation: "Can mention, reply or quote"
|
||||||
canCreateContent: "Can create contents"
|
canCreateContent: "Can create contents"
|
||||||
canUpdateContent: "Can edit contents"
|
canUpdateContent: "Can edit contents"
|
||||||
canDeleteContent: "Can delete contents"
|
canDeleteContent: "Can delete contents"
|
||||||
|
|
|
@ -6558,6 +6558,10 @@ export interface Locale extends ILocale {
|
||||||
* パブリック投稿の許可
|
* パブリック投稿の許可
|
||||||
*/
|
*/
|
||||||
"canPublicNote": string;
|
"canPublicNote": string;
|
||||||
|
/**
|
||||||
|
* メンション、リプライ、引用の許可
|
||||||
|
*/
|
||||||
|
"canInitiateConversation": string;
|
||||||
/**
|
/**
|
||||||
* コンテンツの作成
|
* コンテンツの作成
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1697,6 +1697,7 @@ _role:
|
||||||
gtlAvailable: "グローバルタイムラインの閲覧"
|
gtlAvailable: "グローバルタイムラインの閲覧"
|
||||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||||
canPublicNote: "パブリック投稿の許可"
|
canPublicNote: "パブリック投稿の許可"
|
||||||
|
canInitiateConversation: "メンション、リプライ、引用の許可"
|
||||||
canCreateContent: "コンテンツの作成"
|
canCreateContent: "コンテンツの作成"
|
||||||
canUpdateContent: "コンテンツの編集"
|
canUpdateContent: "コンテンツの編集"
|
||||||
canDeleteContent: "コンテンツの削除"
|
canDeleteContent: "コンテンツの削除"
|
||||||
|
|
|
@ -259,13 +259,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (data.channel != null) data.localOnly = true;
|
if (data.channel != null) data.localOnly = true;
|
||||||
|
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
|
const policies = await this.roleService.getUserPolicies(user.id);
|
||||||
|
|
||||||
if (data.visibility === 'public' && data.channel == null) {
|
if (data.visibility === 'public' && data.channel == null) {
|
||||||
const sensitiveWords = meta.sensitiveWords;
|
const sensitiveWords = meta.sensitiveWords;
|
||||||
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
|
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
this.logger.warn('Visibility changed to home because sensitive words are included', { user: user.id, note: data });
|
this.logger.warn('Visibility changed to home because sensitive words are included', { user: user.id, note: data });
|
||||||
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
|
} else if (policies.canPublicNote === false) {
|
||||||
data.visibility = 'home';
|
data.visibility = 'home';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -379,6 +380,18 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (policies.canInitiateConversation === false) {
|
||||||
|
if (
|
||||||
|
mentionedUsers.some(u => u.id !== user.id)
|
||||||
|
|| (data.reply && data.reply.replyUserId !== user.id)
|
||||||
|
|| (data.visibility === 'specified' && data.visibleUsers?.some(u => u.id !== user.id))
|
||||||
|
|| (this.isQuote(data) && data.renote.userId !== user.id)
|
||||||
|
) {
|
||||||
|
this.logger.error('Request rejected because user has no permission to initiate conversation', { user: user.id, note: data });
|
||||||
|
throw new IdentifiableError('332dd91b-6a00-430a-ac39-620cf60ad34b', 'Notes including mentions, replies, or renotes are not allowed.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
|
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
|
||||||
|
|
||||||
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
|
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
|
||||||
|
|
|
@ -36,6 +36,7 @@ export type RolePolicies = {
|
||||||
gtlAvailable: boolean;
|
gtlAvailable: boolean;
|
||||||
ltlAvailable: boolean;
|
ltlAvailable: boolean;
|
||||||
canPublicNote: boolean;
|
canPublicNote: boolean;
|
||||||
|
canInitiateConversation: boolean;
|
||||||
canCreateContent: boolean;
|
canCreateContent: boolean;
|
||||||
canUpdateContent: boolean;
|
canUpdateContent: boolean;
|
||||||
canDeleteContent: boolean;
|
canDeleteContent: boolean;
|
||||||
|
@ -69,6 +70,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
gtlAvailable: true,
|
gtlAvailable: true,
|
||||||
ltlAvailable: true,
|
ltlAvailable: true,
|
||||||
canPublicNote: true,
|
canPublicNote: true,
|
||||||
|
canInitiateConversation: true,
|
||||||
canCreateContent: true,
|
canCreateContent: true,
|
||||||
canUpdateContent: true,
|
canUpdateContent: true,
|
||||||
canDeleteContent: true,
|
canDeleteContent: true,
|
||||||
|
@ -338,6 +340,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||||
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)),
|
canPublicNote: calc('canPublicNote', 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)),
|
||||||
canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)),
|
canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)),
|
||||||
|
|
|
@ -393,7 +393,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
bannerBlurhash: user.bannerBlurhash,
|
bannerBlurhash: user.bannerBlurhash,
|
||||||
isLocked: user.isLocked,
|
isLocked: user.isLocked,
|
||||||
isSilenced: !policies?.canPublicNote,
|
isSilenced: !policies?.canPublicNote,
|
||||||
isLimited: !(policies?.canCreateContent && policies.canUpdateContent && policies.canDeleteContent),
|
isLimited: !(policies?.canCreateContent && policies.canUpdateContent && policies.canDeleteContent && policies.canInitiateConversation),
|
||||||
isSuspended: user.isSuspended,
|
isSuspended: user.isSuspended,
|
||||||
description: profile!.description,
|
description: profile!.description,
|
||||||
location: profile!.location,
|
location: profile!.location,
|
||||||
|
|
|
@ -212,7 +212,7 @@ 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);
|
const isLimited = !(policies.canCreateContent && policies.canUpdateContent && policies.canDeleteContent && policies.canInitiateConversation);
|
||||||
const isSilenced = !policies.canPublicNote;
|
const isSilenced = !policies.canPublicNote;
|
||||||
|
|
||||||
const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
|
const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||||
|
|
|
@ -75,6 +75,7 @@ export const ROLE_POLICIES = [
|
||||||
'gtlAvailable',
|
'gtlAvailable',
|
||||||
'ltlAvailable',
|
'ltlAvailable',
|
||||||
'canPublicNote',
|
'canPublicNote',
|
||||||
|
'canInitiateConversation',
|
||||||
'canCreateContent',
|
'canCreateContent',
|
||||||
'canUpdateContent',
|
'canUpdateContent',
|
||||||
'canDeleteContent',
|
'canDeleteContent',
|
||||||
|
|
|
@ -160,6 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInitiateConversation, 'canInitiateConversation'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.canInitiateConversation }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.canInitiateConversation.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.canInitiateConversation.value ? i18n.ts.yes : i18n.ts.no }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canInitiateConversation)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.canInitiateConversation.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkSwitch v-model="role.policies.canInitiateConversation.value" :disabled="role.policies.canInitiateConversation.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkRange v-model="role.policies.canInitiateConversation.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.canCreateContent, 'canCreateContent'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canCreateContent, 'canCreateContent'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canCreateContent }}</template>
|
<template #label>{{ i18n.ts._role._options.canCreateContent }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
|
|
@ -48,6 +48,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</MkFolder>
|
</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>
|
||||||
|
<MkSwitch v-model="policies.canInitiateConversation">
|
||||||
|
<template #label>{{ i18n.ts.enable }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canCreateContent, 'canCreateContent'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canCreateContent, 'canCreateContent'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canCreateContent }}</template>
|
<template #label>{{ i18n.ts._role._options.canCreateContent }}</template>
|
||||||
<template #suffix>{{ policies.canCreateContent ? i18n.ts.yes : i18n.ts.no }}</template>
|
<template #suffix>{{ policies.canCreateContent ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
|
Loading…
Reference in New Issue