Merge branch 'develop' into mkjs-n

This commit is contained in:
tamaina
2023-05-10 14:20:07 +00:00
14 changed files with 217 additions and 298 deletions

View File

@@ -3,6 +3,7 @@ import * as mfm from 'mfm-js';
import { In, DataSource } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import RE2 from 're2';
import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
@@ -238,7 +239,8 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.channel != null) data.localOnly = true;
if (data.visibility === 'public' && data.channel == null) {
if ((data.text != null) && (await this.metaService.fetch()).sensitiveWords.some(w => data.text!.includes(w))) {
const sensitiveWords = (await this.metaService.fetch()).sensitiveWords;
if (this.isSensitive(data, sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home';
@@ -670,6 +672,31 @@ export class NoteCreateService implements OnApplicationShutdown {
// Register to search database
this.index(note);
}
@bindThis
private isSensitive(note: Option, sensitiveWord: string[]): boolean {
if (sensitiveWord.length > 0) {
const text = note.cw ?? note.text ?? '';
if (text === '') return false;
const matched = sensitiveWord.some(filter => {
// represents RegExp
const regexp = filter.match(/^\/(.+)\/(.*)$/);
// This should never happen due to input sanitisation.
if (!regexp) {
const words = filter.split(' ');
return words.every(keyword => text.includes(keyword));
}
try {
return new RE2(regexp[1], regexp[2]).test(text);
} catch (err) {
// This should never happen due to input sanitisation.
return false;
}
});
if (matched) return true;
}
return false;
}
@bindThis
private incRenoteCount(renote: Note) {

View File

@@ -541,6 +541,61 @@ describe('Note', () => {
assert.strictEqual(res.status, 400);
});
test('センシティブな投稿はhomeになる (単語指定)', async () => {
const sensitive = await api('admin/update-meta', {
sensitiveWords: [
"test",
]
}, alice);
assert.strictEqual(sensitive.status, 204);
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
text: 'hogetesthuge',
}, alice);
assert.strictEqual(note1.status, 200);
assert.strictEqual(note1.body.createdNote.visibility, 'home');
});
test('センシティブな投稿はhomeになる (正規表現)', async () => {
const sensitive = await api('admin/update-meta', {
sensitiveWords: [
"/Test/i",
]
}, alice);
assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', {
text: 'hogetesthuge',
}, alice);
assert.strictEqual(note2.status, 200);
assert.strictEqual(note2.body.createdNote.visibility, 'home');
});
test('センシティブな投稿はhomeになる (スペースアンド)', async () => {
const sensitive = await api('admin/update-meta', {
sensitiveWords: [
"Test hoge"
]
}, alice);
assert.strictEqual(sensitive.status, 204);
const note2 = await api('/notes/create', {
text: 'hogeTesthuge',
}, alice);
assert.strictEqual(note2.status, 200);
assert.strictEqual(note2.body.createdNote.visibility, 'home');
});
});
describe('notes/delete', () => {

View File

@@ -1,144 +0,0 @@
<template>
<div
class="ziffeoms"
:class="{ disabled, checked }"
>
<input
ref="input"
type="checkbox"
:disabled="disabled"
@keydown.enter="toggle"
>
<span ref="button" v-adaptive-border v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle">
<i class="check ti ti-check"></i>
</span>
<span class="label">
<!-- TODO: 無名slotの方は廃止 -->
<span @click="toggle"><slot name="label"></slot><slot></slot></span>
<p class="caption"><slot name="caption"></slot></p>
</span>
</div>
</template>
<script lang="ts" setup>
import { toRefs, Ref } from 'vue';
import * as os from '@/os';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { i18n } from '@/i18n';
const props = defineProps<{
modelValue: boolean | Ref<boolean>;
disabled?: boolean;
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', v: boolean): void;
}>();
let button = $shallowRef<HTMLElement>();
const checked = toRefs(props).modelValue;
const toggle = () => {
if (props.disabled) return;
emit('update:modelValue', !checked.value);
if (!checked.value) {
const rect = button.getBoundingClientRect();
const x = rect.left + (button.offsetWidth / 2);
const y = rect.top + (button.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y, particle: false }, {}, 'end');
}
};
</script>
<style lang="scss" scoped>
.ziffeoms {
position: relative;
display: flex;
transition: all 0.2s ease;
> * {
user-select: none;
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin: 0;
box-sizing: border-box;
width: 23px;
height: 23px;
outline: none;
background: var(--panel);
border: solid 1px var(--panel);
border-radius: 4px;
cursor: pointer;
transition: inherit;
> .check {
margin: auto;
opacity: 0;
color: var(--fgOnAccent);
font-size: 13px;
transform: scale(0.5);
transition: all 0.2s ease;
}
}
&:hover {
> .button {
border-color: var(--inputBorderHover) !important;
}
}
> .label {
margin-left: 12px;
margin-top: 2px;
display: block;
transition: inherit;
color: var(--fg);
> span {
display: block;
line-height: 20px;
cursor: pointer;
transition: inherit;
}
> .caption {
margin: 8px 0 0 0;
color: var(--fgTransparentWeak);
font-size: 0.85em;
&:empty {
display: none;
}
}
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.checked {
> .button {
background-color: var(--accent) !important;
border-color: var(--accent) !important;
> .check {
opacity: 1;
transform: scale(1);
}
}
}
}
</style>

View File

@@ -1,8 +1,7 @@
<template>
<div
v-adaptive-border
class="novjtctn"
:class="{ disabled, checked }"
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
:aria-checked="checked"
:aria-disabled="disabled"
@click="toggle"
@@ -10,11 +9,12 @@
<input
type="radio"
:disabled="disabled"
:class="$style.input"
>
<span class="button">
<span :class="$style.button">
<span></span>
</span>
<span class="label"><slot></slot></span>
<span :class="$style.label"><slot></slot></span>
</div>
</template>
@@ -39,8 +39,8 @@ function toggle(): void {
}
</script>
<style lang="scss" scoped>
.novjtctn {
<style lang="scss" module>
.root {
position: relative;
display: inline-block;
text-align: left;
@@ -53,17 +53,11 @@ function toggle(): void {
border-radius: 6px;
font-size: 90%;
transition: all 0.2s;
> * {
user-select: none;
}
user-select: none;
&.disabled {
opacity: 0.6;
&, * {
cursor: not-allowed !important;
}
cursor: not-allowed !important;
}
&:hover {
@@ -74,10 +68,7 @@ function toggle(): void {
background-color: var(--accentedBg) !important;
border-color: var(--accentedBg) !important;
color: var(--accent);
&, * {
cursor: default !important;
}
cursor: default !important;
> .button {
border-color: var(--accent);
@@ -89,44 +80,44 @@ function toggle(): void {
}
}
}
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
.input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: absolute;
width: 14px;
height: 14px;
background: none;
border: solid 2px var(--inputBorder);
border-radius: 100%;
transition: inherit;
.button {
position: absolute;
width: 14px;
height: 14px;
background: none;
border: solid 2px var(--inputBorder);
border-radius: 100%;
transition: inherit;
&:after {
content: '';
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
> .label {
margin-left: 28px;
&:after {
content: '';
display: block;
line-height: 20px;
cursor: pointer;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
}
.label {
margin-left: 28px;
display: block;
line-height: 20px;
cursor: pointer;
}
</style>

View File

@@ -1,21 +1,19 @@
<template>
<div
class="ziffeomt"
:class="{ disabled, checked }"
>
<div :class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]">
<input
ref="input"
type="checkbox"
:disabled="disabled"
:class="$style.input"
@keydown.enter="toggle"
>
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle">
<div class="knob"></div>
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" :class="$style.button" data-cy-switch-toggle @click.prevent="toggle">
<div :class="$style.knob"></div>
</span>
<span class="label">
<span :class="$style.body">
<!-- TODO: 無名slotの方は廃止 -->
<span @click="toggle"><slot name="label"></slot><slot></slot></span>
<p class="caption"><slot name="caption"></slot></p>
<span :class="$style.label" @click="toggle"><slot name="label"></slot><slot></slot></span>
<p :class="$style.caption"><slot name="caption"></slot></p>
</span>
</div>
</template>
@@ -45,52 +43,12 @@ const toggle = () => {
};
</script>
<style lang="scss" scoped>
.ziffeomt {
<style lang="scss" module>
.root {
position: relative;
display: flex;
transition: all 0.2s ease;
> * {
user-select: none;
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin: 0;
box-sizing: border-box;
width: 32px;
height: 23px;
outline: none;
background: var(--switchOffBg);
background-clip: content-box;
border: solid 1px var(--switchOffBg);
border-radius: 999px;
cursor: pointer;
transition: inherit;
user-select: none;
> .knob {
position: absolute;
top: 3px;
left: 3px;
width: 15px;
height: 15px;
background: var(--switchOffFg);
border-radius: 999px;
transition: all 0.2s ease;
}
}
user-select: none;
&:hover {
> .button {
@@ -98,31 +56,6 @@ const toggle = () => {
}
}
> .label {
margin-left: 12px;
margin-top: 2px;
display: block;
transition: inherit;
color: var(--fg);
> span {
display: block;
line-height: 20px;
cursor: pointer;
transition: inherit;
}
> .caption {
margin: 8px 0 0 0;
color: var(--fgTransparentWeak);
font-size: 0.85em;
&:empty {
display: none;
}
}
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
@@ -140,4 +73,66 @@ const toggle = () => {
}
}
}
.input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
.button {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin: 0;
box-sizing: border-box;
width: 32px;
height: 23px;
outline: none;
background: var(--switchOffBg);
background-clip: content-box;
border: solid 1px var(--switchOffBg);
border-radius: 999px;
cursor: pointer;
transition: inherit;
user-select: none;
}
.knob {
position: absolute;
top: 3px;
left: 3px;
width: 15px;
height: 15px;
background: var(--switchOffFg);
border-radius: 999px;
transition: all 0.2s ease;
}
.body {
margin-left: 12px;
margin-top: 2px;
display: block;
transition: inherit;
color: var(--fg);
}
.label {
display: block;
line-height: 20px;
cursor: pointer;
transition: inherit;
}
.caption {
margin: 8px 0 0 0;
color: var(--fgTransparentWeak);
font-size: 0.85em;
&:empty {
display: none;
}
}
</style>

View File

@@ -32,6 +32,7 @@
<component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
</div>
</template>
<script lang="ts">
export type Widget = {
name: string;
@@ -42,6 +43,7 @@ export type DefaultStoredWidget = {
place: string | null;
} & Widget;
</script>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
import { v4 as uuid } from 'uuid';

View File

@@ -27,7 +27,7 @@
<MkTextarea v-model="sensitiveWords">
<template #label>{{ i18n.ts.sensitiveWords }}</template>
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template>
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
</div>
</FormSuspense>

View File

@@ -28,9 +28,9 @@
<MkFoldableSection ref="tagsEl" :foldable="true" :expanded="false" class="_margin">
<template #header><i class="ti ti-hash ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.popularTags }}</template>
<div class="vxjfqztj">
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" class="local">{{ tag.tag }}</MkA>
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`">{{ tag.tag }}</MkA>
<div>
<MkA v-for="tag in tagsLocal" :key="'local:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px; font-weight: bold;">{{ tag.tag }}</MkA>
<MkA v-for="tag in tagsRemote" :key="'remote:' + tag.tag" :to="`/user-tags/${tag.tag}`" style="margin-right: 16px;">{{ tag.tag }}</MkA>
</div>
</MkFoldableSection>
@@ -132,15 +132,3 @@ os.api('hashtags/list', {
tagsRemote = tags;
});
</script>
<style lang="scss" scoped>
.vxjfqztj {
> * {
margin-right: 16px;
&.local {
font-weight: bold;
}
}
}
</style>