feat: sensitive word
This commit is contained in:
parent
b18df999cd
commit
7f16b50e73
10 changed files with 122 additions and 1 deletions
|
@ -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): フォロー申請のボタンのデザインを改善
|
||||||
|
|
|
@ -971,6 +971,8 @@ likeOnly: "いいねのみ"
|
||||||
likeOnlyForRemote: "リモートからはいいねのみ"
|
likeOnlyForRemote: "リモートからはいいねのみ"
|
||||||
rolesAssignedToMe: "自分に割り当てられたロール"
|
rolesAssignedToMe: "自分に割り当てられたロール"
|
||||||
resetPasswordConfirm: "パスワードリセットしますか?"
|
resetPasswordConfirm: "パスワードリセットしますか?"
|
||||||
|
sensitiveWords: "センシティブワード"
|
||||||
|
sensitiveWordsDescription: "設定したワードが含まれるノートの公開範囲をホームにします。改行で区切って複数設定できます。"
|
||||||
|
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "獲得日時"
|
earnedAt: "獲得日時"
|
||||||
|
|
11
packages/backend/migration/1678694614599-sensitive-words.js
Normal file
11
packages/backend/migration/1678694614599-sensitive-words.js
Normal 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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
73
packages/frontend/src/pages/admin/moderation.vue
Normal file
73
packages/frontend/src/pages/admin/moderation.vue
Normal 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>
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in a new issue