feat: sensitive word

This commit is contained in:
syuilo 2023-03-13 17:37:22 +09:00
parent b18df999cd
commit 7f16b50e73
10 changed files with 122 additions and 1 deletions

View File

@ -16,6 +16,7 @@ You should also include the user name that made the change.
- ユーザーごとにRenoteをミュートできるように - ユーザーごとにRenoteをミュートできるように
- ノートごとに絵文字リアクションを受け取るか設定できるように - ノートごとに絵文字リアクションを受け取るか設定できるように
- ロールの並び順を設定可能に - ロールの並び順を設定可能に
- 指定した文字列を含む投稿の公開範囲をホームにできるように
- enhance(client): 設定から自分のロールを確認できるように - enhance(client): 設定から自分のロールを確認できるように
- enhance(client): DM作成時にメンションも含むように - enhance(client): DM作成時にメンションも含むように
- enhance(client): フォロー申請のボタンのデザインを改善 - enhance(client): フォロー申請のボタンのデザインを改善

View File

@ -971,6 +971,8 @@ likeOnly: "いいねのみ"
likeOnlyForRemote: "リモートからはいいねのみ" likeOnlyForRemote: "リモートからはいいねのみ"
rolesAssignedToMe: "自分に割り当てられたロール" rolesAssignedToMe: "自分に割り当てられたロール"
resetPasswordConfirm: "パスワードリセットしますか?" resetPasswordConfirm: "パスワードリセットしますか?"
sensitiveWords: "センシティブワード"
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
_achievements: _achievements:
earnedAt: "獲得日時" earnedAt: "獲得日時"

View File

@ -0,0 +1,11 @@
export class sensitiveWords1678694614599 {
name = 'sensitiveWords1678694614599'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "sensitiveWords" character varying(1024) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "sensitiveWords"`);
}
}

View File

@ -44,6 +44,7 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5); const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
@ -192,6 +193,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private apDeliverManagerService: ApDeliverManagerService, private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private roleService: RoleService, private roleService: RoleService,
private metaService: MetaService,
private notesChart: NotesChart, private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart, private perUserNotesChart: PerUserNotesChart,
private activeUsersChart: ActiveUsersChart, private activeUsersChart: ActiveUsersChart,
@ -230,7 +232,9 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.channel != null) data.localOnly = true; if (data.channel != null) data.localOnly = true;
if (data.visibility === 'public' && data.channel == null) { if (data.visibility === 'public' && data.channel == null) {
if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { if ((data.text != null) && (await this.metaService.fetch()).sensitiveWords.some(w => data.text!.includes(w))) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home'; data.visibility = 'home';
} }
} }

View File

@ -67,6 +67,11 @@ export class Meta {
}) })
public blockedHosts: string[]; public blockedHosts: string[];
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public sensitiveWords: string[];
@Column('varchar', { @Column('varchar', {
length: 1024, length: 1024,
nullable: true, nullable: true,

View File

@ -110,6 +110,14 @@ export const meta = {
optional: false, nullable: false, optional: false, nullable: false,
}, },
}, },
sensitiveWords: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
hcaptchaSecretKey: { hcaptchaSecretKey: {
type: 'string', type: 'string',
optional: true, nullable: true, optional: true, nullable: true,
@ -295,6 +303,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
pinnedUsers: instance.pinnedUsers, pinnedUsers: instance.pinnedUsers,
hiddenTags: instance.hiddenTags, hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts, blockedHosts: instance.blockedHosts,
sensitiveWords: instance.sensitiveWords,
hcaptchaSecretKey: instance.hcaptchaSecretKey, hcaptchaSecretKey: instance.hcaptchaSecretKey,
recaptchaSecretKey: instance.recaptchaSecretKey, recaptchaSecretKey: instance.recaptchaSecretKey,
turnstileSecretKey: instance.turnstileSecretKey, turnstileSecretKey: instance.turnstileSecretKey,

View File

@ -27,6 +27,9 @@ export const paramDef = {
blockedHosts: { type: 'array', nullable: true, items: { blockedHosts: { type: 'array', nullable: true, items: {
type: 'string', type: 'string',
} }, } },
sensitiveWords: { type: 'array', nullable: true, items: {
type: 'string',
} },
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' }, themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true }, mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true }, bannerUrl: { type: 'string', nullable: true },
@ -127,6 +130,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.blockedHosts = ps.blockedHosts.filter(Boolean).map(x => x.toLowerCase()); set.blockedHosts = ps.blockedHosts.filter(Boolean).map(x => x.toLowerCase());
} }
if (Array.isArray(ps.sensitiveWords)) {
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
}
if (ps.themeColor !== undefined) { if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor; set.themeColor = ps.themeColor;
} }

View File

@ -143,6 +143,11 @@ const menuDef = $computed(() => [{
text: i18n.ts.general, text: i18n.ts.general,
to: '/admin/settings', to: '/admin/settings',
active: currentPage?.route.name === 'settings', active: currentPage?.route.name === 'settings',
}, {
icon: 'ti ti-shield',
text: i18n.ts.moderation,
to: '/admin/moderation',
active: currentPage?.route.name === 'moderation',
}, { }, {
icon: 'ti ti-mail', icon: 'ti ti-mail',
text: i18n.ts.emailServer, text: i18n.ts.emailServer,

View File

@ -0,0 +1,73 @@
<template>
<div>
<MkStickyContainer>
<template #header><XHeader :tabs="headerTabs"/></template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_gaps_m">
<FormSection first>
<div class="_gaps_m">
<MkTextarea v-model="sensitiveWords">
<template #label>{{ i18n.ts.sensitiveWords }}</template>
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template>
</MkTextarea>
</div>
</FormSection>
</div>
</FormSuspense>
</MkSpacer>
<template #footer>
<div :class="$style.footer">
<MkSpacer :content-max="700" :margin-min="16" :margin-max="16">
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</MkSpacer>
</div>
</template>
</MkStickyContainer>
</div>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import MkButton from '@/components/MkButton.vue';
let sensitiveWords: string = $ref('');
async function init() {
const meta = await os.api('admin/meta');
sensitiveWords = meta.pinnedUsers.join('\n');
}
function save() {
os.apiWithDialog('admin/update-meta', {
sensitiveWords: sensitiveWords.split('\n'),
}).then(() => {
fetchInstance();
});
}
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.moderation,
icon: 'ti ti-shield',
});
</script>
<style lang="scss" module>
.footer {
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
</style>

View File

@ -384,6 +384,10 @@ export const routes = [{
path: '/settings', path: '/settings',
name: 'settings', name: 'settings',
component: page(() => import('./pages/admin/settings.vue')), component: page(() => import('./pages/admin/settings.vue')),
}, {
path: '/moderation',
name: 'moderation',
component: page(() => import('./pages/admin/moderation.vue')),
}, { }, {
path: '/email-settings', path: '/email-settings',
name: 'email-settings', name: 'email-settings',