parent
b5a1fdd4c7
commit
cf43dd6ec5
@ -553,6 +553,17 @@ emptyToDisableSmtpAuth: "ユーザー名とパスワードを空欄にするこ
|
|||||||
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
|
smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
|
||||||
smtpSecureInfo: "STARTTLS使用時はオフにします。"
|
smtpSecureInfo: "STARTTLS使用時はオフにします。"
|
||||||
testEmail: "配信テスト"
|
testEmail: "配信テスト"
|
||||||
|
wordMute: "ワードミュート"
|
||||||
|
userSaysSomething: "{name}が何かを言いました"
|
||||||
|
|
||||||
|
_wordMute:
|
||||||
|
muteWords: "ミュートするワード"
|
||||||
|
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
|
||||||
|
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
|
||||||
|
softDescription: "指定した条件のノートをタイムラインから隠します。"
|
||||||
|
hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。"
|
||||||
|
soft: "ソフト"
|
||||||
|
hard: "ハード"
|
||||||
|
|
||||||
_theme:
|
_theme:
|
||||||
explore: "テーマを探す"
|
explore: "テーマを探す"
|
||||||
|
30
migration/1595771249699-word-mute.ts
Normal file
30
migration/1595771249699-word-mute.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class wordMute1595771249699 implements MigrationInterface {
|
||||||
|
name = 'wordMute1595771249699'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "muted_note" ("id" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_897e2eff1c0b9b64e55ca1418a4" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_70ab9786313d78e4201d81cdb8" ON "muted_note" ("noteId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d8e07aa18c2d64e86201601aec" ON "muted_note" ("userId") `);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a8c6bfd637d3f1d67a27c48e27" ON "muted_note" ("noteId", "userId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "enableWordMute" boolean NOT NULL DEFAULT false`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "mutedWords" jsonb NOT NULL DEFAULT '[]'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_3befe6f999c86aff06eb0257b4" ON "user_profile" ("enableWordMute") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_70ab9786313d78e4201d81cdb89" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "muted_note" ADD CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_d8e07aa18c2d64e86201601aec1"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "muted_note" DROP CONSTRAINT "FK_70ab9786313d78e4201d81cdb89"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_3befe6f999c86aff06eb0257b4"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mutedWords"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "enableWordMute"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_a8c6bfd637d3f1d67a27c48e27"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_d8e07aa18c2d64e86201601aec"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_70ab9786313d78e4201d81cdb8"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "muted_note"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
18
migration/1595782306083-word-mute2.ts
Normal file
18
migration/1595782306083-word-mute2.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||||
|
|
||||||
|
export class wordMute21595782306083 implements MigrationInterface {
|
||||||
|
name = 'wordMute21595782306083'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TYPE "muted_note_reason_enum" AS ENUM('word', 'manual', 'spam', 'other')`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "muted_note" ADD "reason" "muted_note_reason_enum" NOT NULL`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_636e977ff90b23676fb5624b25" ON "muted_note" ("reason") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "IDX_636e977ff90b23676fb5624b25"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "muted_note" DROP COLUMN "reason"`);
|
||||||
|
await queryRunner.query(`DROP TYPE "muted_note_reason_enum"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -204,6 +204,7 @@
|
|||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"randomcolor": "0.5.4",
|
"randomcolor": "0.5.4",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
|
"re2": "1.15.4",
|
||||||
"recaptcha-promise": "0.1.3",
|
"recaptcha-promise": "0.1.3",
|
||||||
"reconnecting-websocket": "4.4.0",
|
"reconnecting-websocket": "4.4.0",
|
||||||
"redis": "3.0.2",
|
"redis": "3.0.2",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="note _panel"
|
class="note _panel"
|
||||||
|
v-if="!muted"
|
||||||
v-show="!isDeleted"
|
v-show="!isDeleted"
|
||||||
:tabindex="!isDeleted ? '-1' : null"
|
:tabindex="!isDeleted ? '-1' : null"
|
||||||
:class="{ renote: isRenote }"
|
:class="{ renote: isRenote }"
|
||||||
@ -84,6 +85,13 @@
|
|||||||
</article>
|
</article>
|
||||||
<x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
|
<x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="_panel muted" @click="muted = false">
|
||||||
|
<i18n path="userSaysSomething" tag="small">
|
||||||
|
<router-link class="name" :to="appearNote.user | userPage" v-user-preview="appearNote.userId" place="name">
|
||||||
|
<mk-user-name :user="appearNote.user"/>
|
||||||
|
</router-link>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -105,6 +113,7 @@ import pleaseLogin from '../scripts/please-login';
|
|||||||
import { focusPrev, focusNext } from '../scripts/focus';
|
import { focusPrev, focusNext } from '../scripts/focus';
|
||||||
import { url } from '../config';
|
import { url } from '../config';
|
||||||
import copyToClipboard from '../scripts/copy-to-clipboard';
|
import copyToClipboard from '../scripts/copy-to-clipboard';
|
||||||
|
import { checkWordMute } from '../scripts/check-word-mute';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
@ -142,6 +151,7 @@ export default Vue.extend({
|
|||||||
replies: [],
|
replies: [],
|
||||||
showContent: false,
|
showContent: false,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
|
muted: false,
|
||||||
myReaction: null,
|
myReaction: null,
|
||||||
reactions: {},
|
reactions: {},
|
||||||
emojis: [],
|
emojis: [],
|
||||||
@ -227,15 +237,16 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
async created() {
|
||||||
this.emojis = [...this.appearNote.emojis];
|
|
||||||
this.reactions = { ...this.appearNote.reactions };
|
|
||||||
this.myReaction = this.appearNote.myReaction;
|
|
||||||
|
|
||||||
if (this.$store.getters.isSignedIn) {
|
if (this.$store.getters.isSignedIn) {
|
||||||
this.connection = this.$root.stream;
|
this.connection = this.$root.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emojis = [...this.appearNote.emojis];
|
||||||
|
this.reactions = { ...this.appearNote.reactions };
|
||||||
|
this.myReaction = this.appearNote.myReaction;
|
||||||
|
this.muted = await checkWordMute(this.appearNote, this.$store.state.i, this.$store.state.settings.mutedWords);
|
||||||
|
|
||||||
if (this.detail) {
|
if (this.detail) {
|
||||||
this.$root.api('notes/children', {
|
this.$root.api('notes/children', {
|
||||||
noteId: this.appearNote.id,
|
noteId: this.appearNote.id,
|
||||||
@ -976,4 +987,10 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
42
src/client/components/tab.vue
Normal file
42
src/client/components/tab.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pxhvhrfw" v-size="[{ max: 500 }]">
|
||||||
|
<button v-for="item in items" class="_button" @click="$emit('input', item.value)" :class="{ active: value === item.value }" :key="item.value">{{ item.label }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.pxhvhrfw {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 11px 8px 8px 8px;
|
||||||
|
border-bottom: solid 3px transparent;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--accent);
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.max-width_500px {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -27,6 +27,7 @@
|
|||||||
<x-import-export/>
|
<x-import-export/>
|
||||||
<x-drive/>
|
<x-drive/>
|
||||||
<x-mute-block/>
|
<x-mute-block/>
|
||||||
|
<x-word-mute/>
|
||||||
<x-security/>
|
<x-security/>
|
||||||
<x-2fa/>
|
<x-2fa/>
|
||||||
<x-integration/>
|
<x-integration/>
|
||||||
@ -47,6 +48,7 @@ import XImportExport from './import-export.vue';
|
|||||||
import XDrive from './drive.vue';
|
import XDrive from './drive.vue';
|
||||||
import XReactionSetting from './reaction.vue';
|
import XReactionSetting from './reaction.vue';
|
||||||
import XMuteBlock from './mute-block.vue';
|
import XMuteBlock from './mute-block.vue';
|
||||||
|
import XWordMute from './word-mute.vue';
|
||||||
import XSecurity from './security.vue';
|
import XSecurity from './security.vue';
|
||||||
import X2fa from './2fa.vue';
|
import X2fa from './2fa.vue';
|
||||||
import XIntegration from './integration.vue';
|
import XIntegration from './integration.vue';
|
||||||
@ -68,6 +70,7 @@ export default Vue.extend({
|
|||||||
XDrive,
|
XDrive,
|
||||||
XReactionSetting,
|
XReactionSetting,
|
||||||
XMuteBlock,
|
XMuteBlock,
|
||||||
|
XWordMute,
|
||||||
XSecurity,
|
XSecurity,
|
||||||
X2fa,
|
X2fa,
|
||||||
XIntegration,
|
XIntegration,
|
||||||
|
77
src/client/pages/my-settings/word-mute.vue
Normal file
77
src/client/pages/my-settings/word-mute.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<section class="_card">
|
||||||
|
<div class="_title"><fa :icon="faCommentSlash"/> {{ $t('wordMute') }}</div>
|
||||||
|
<div class="_content _noPad">
|
||||||
|
<mk-tab v-model="tab" :items="[{ label: $t('_wordMute.soft'), value: 'soft' }, { label: $t('_wordMute.hard'), value: 'hard' }]"/>
|
||||||
|
</div>
|
||||||
|
<div class="_content" v-show="tab === 'soft'">
|
||||||
|
<mk-info>{{ $t('_wordMute.softDescription') }}</mk-info>
|
||||||
|
<mk-textarea v-model="softMutedWords">
|
||||||
|
<span>{{ $t('_wordMute.muteWords') }}</span>
|
||||||
|
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
|
||||||
|
</mk-textarea>
|
||||||
|
</div>
|
||||||
|
<div class="_content" v-show="tab === 'hard'">
|
||||||
|
<mk-info>{{ $t('_wordMute.hardDescription') }}</mk-info>
|
||||||
|
<mk-textarea v-model="hardMutedWords">
|
||||||
|
<span>{{ $t('_wordMute.muteWords') }}</span>
|
||||||
|
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
|
||||||
|
</mk-textarea>
|
||||||
|
</div>
|
||||||
|
<div class="_footer">
|
||||||
|
<mk-button @click="save()" primary inline :disabled="!changed"><fa :icon="faSave"/> {{ $t('save') }}</mk-button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import MkButton from '../../components/ui/button.vue';
|
||||||
|
import MkTextarea from '../../components/ui/textarea.vue';
|
||||||
|
import MkTab from '../../components/tab.vue';
|
||||||
|
import MkInfo from '../../components/ui/info.vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
MkButton,
|
||||||
|
MkTextarea,
|
||||||
|
MkTab,
|
||||||
|
MkInfo,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tab: 'soft',
|
||||||
|
softMutedWords: '',
|
||||||
|
hardMutedWords: '',
|
||||||
|
changed: false,
|
||||||
|
faCommentSlash, faSave,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
softMutedWords() {
|
||||||
|
this.changed = true;
|
||||||
|
},
|
||||||
|
hardMutedWords() {
|
||||||
|
this.changed = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.softMutedWords = this.$store.state.settings.mutedWords.map(x => x.join(' ')).join('\n');
|
||||||
|
this.hardMutedWords = this.$store.state.i.mutedWords.map(x => x.join(' ')).join('\n');
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async save() {
|
||||||
|
this.$store.dispatch('settings/set', { key: 'mutedWords', value: this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')) });
|
||||||
|
await this.$root.api('i/update', {
|
||||||
|
mutedWords: this.hardMutedWords.trim().split('\n').map(x => x.trim().split(' ')),
|
||||||
|
});
|
||||||
|
this.changed = false;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
26
src/client/scripts/check-word-mute.ts
Normal file
26
src/client/scripts/check-word-mute.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export async function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): Promise<boolean> {
|
||||||
|
// 自分自身
|
||||||
|
if (me && (note.userId === me.id)) return false;
|
||||||
|
|
||||||
|
const words = mutedWords
|
||||||
|
// Clean up
|
||||||
|
.map(xs => xs.filter(x => x !== ''))
|
||||||
|
.filter(xs => xs.length > 0);
|
||||||
|
|
||||||
|
if (words.length > 0) {
|
||||||
|
if (note.text == null) return false;
|
||||||
|
|
||||||
|
const matched = words.some(and =>
|
||||||
|
and.every(keyword => {
|
||||||
|
const regexp = keyword.match(/^\/(.+)\/(.*)$/);
|
||||||
|
if (regexp) {
|
||||||
|
return new RegExp(regexp[1], regexp[2]).test(note.text!);
|
||||||
|
}
|
||||||
|
return note.text!.includes(keyword);
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (matched) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
@ -18,6 +18,7 @@ export const defaultSettings = {
|
|||||||
pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
|
pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
|
||||||
memo: null,
|
memo: null,
|
||||||
reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
|
reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
|
||||||
|
mutedWords: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultDeviceUserSettings = {
|
export const defaultDeviceUserSettings = {
|
||||||
|
@ -355,6 +355,10 @@ hr {
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&._noPad {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
& + ._content {
|
& + ._content {
|
||||||
border-top: solid 1px var(--divider);
|
border-top: solid 1px var(--divider);
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ import { PromoNote } from '../models/entities/promo-note';
|
|||||||
import { PromoRead } from '../models/entities/promo-read';
|
import { PromoRead } from '../models/entities/promo-read';
|
||||||
import { program } from '../argv';
|
import { program } from '../argv';
|
||||||
import { Relay } from '../models/entities/relay';
|
import { Relay } from '../models/entities/relay';
|
||||||
|
import { MutedNote } from '../models/entities/muted-note';
|
||||||
|
|
||||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||||
|
|
||||||
@ -151,6 +152,7 @@ export const entities = [
|
|||||||
ReversiGame,
|
ReversiGame,
|
||||||
ReversiMatching,
|
ReversiMatching,
|
||||||
Relay,
|
Relay,
|
||||||
|
MutedNote,
|
||||||
...charts as any
|
...charts as any
|
||||||
];
|
];
|
||||||
|
|
||||||
|
39
src/misc/check-word-mute.ts
Normal file
39
src/misc/check-word-mute.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
const RE2 = require('re2');
|
||||||
|
import { Note } from '../models/entities/note';
|
||||||
|
import { User } from '../models/entities/user';
|
||||||
|
|
||||||
|
type NoteLike = {
|
||||||
|
userId: Note['userId'];
|
||||||
|
text: Note['text'];
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserLike = {
|
||||||
|
id: User['id'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: string[][]): Promise<boolean> {
|
||||||
|
// 自分自身
|
||||||
|
if (me && (note.userId === me.id)) return false;
|
||||||
|
|
||||||
|
const words = mutedWords
|
||||||
|
// Clean up
|
||||||
|
.map(xs => xs.filter(x => x !== ''))
|
||||||
|
.filter(xs => xs.length > 0);
|
||||||
|
|
||||||
|
if (words.length > 0) {
|
||||||
|
if (note.text == null) return false;
|
||||||
|
|
||||||
|
const matched = words.some(and =>
|
||||||
|
and.every(keyword => {
|
||||||
|
const regexp = keyword.match(/^\/(.+)\/(.*)$/);
|
||||||
|
if (regexp) {
|
||||||
|
return new RE2(regexp[1], regexp[2]).test(note.text!);
|
||||||
|
}
|
||||||
|
return note.text!.includes(keyword);
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (matched) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
48
src/models/entities/muted-note.ts
Normal file
48
src/models/entities/muted-note.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Entity, Index, JoinColumn, Column, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||||
|
import { Note } from './note';
|
||||||
|
import { User } from './user';
|
||||||
|
import { id } from '../id';
|
||||||
|
import { mutedNoteReasons } from '../../types';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Index(['noteId', 'userId'], { unique: true })
|
||||||
|
export class MutedNote {
|
||||||
|
@PrimaryColumn(id())
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
comment: 'The note ID.'
|
||||||
|
})
|
||||||
|
public noteId: Note['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => Note, {
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public note: Note | null;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({
|
||||||
|
...id(),
|
||||||
|
comment: 'The user ID.'
|
||||||
|
})
|
||||||
|
public userId: User['id'];
|
||||||
|
|
||||||
|
@ManyToOne(type => User, {
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
@JoinColumn()
|
||||||
|
public user: User | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ミュートされた理由。
|
||||||
|
*/
|
||||||
|
@Index()
|
||||||
|
@Column('enum', {
|
||||||
|
enum: mutedNoteReasons,
|
||||||
|
comment: 'The reason of the MutedNote.'
|
||||||
|
})
|
||||||
|
public reason: typeof mutedNoteReasons[number];
|
||||||
|
}
|
@ -147,6 +147,17 @@ export class UserProfile {
|
|||||||
})
|
})
|
||||||
public integrations: Record<string, any>;
|
public integrations: Record<string, any>;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public enableWordMute: boolean;
|
||||||
|
|
||||||
|
@Column('jsonb', {
|
||||||
|
default: []
|
||||||
|
})
|
||||||
|
public mutedWords: string[][];
|
||||||
|
|
||||||
//#region Denormalized fields
|
//#region Denormalized fields
|
||||||
@Index()
|
@Index()
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
|
@ -53,6 +53,7 @@ import { PromoNote } from './entities/promo-note';
|
|||||||
import { PromoRead } from './entities/promo-read';
|
import { PromoRead } from './entities/promo-read';
|
||||||
import { EmojiRepository } from './repositories/emoji';
|
import { EmojiRepository } from './repositories/emoji';
|
||||||
import { RelayRepository } from './repositories/relay';
|
import { RelayRepository } from './repositories/relay';
|
||||||
|
import { MutedNote } from './entities/muted-note';
|
||||||
|
|
||||||
export const Announcements = getRepository(Announcement);
|
export const Announcements = getRepository(Announcement);
|
||||||
export const AnnouncementReads = getRepository(AnnouncementRead);
|
export const AnnouncementReads = getRepository(AnnouncementRead);
|
||||||
@ -108,3 +109,4 @@ export const AntennaNotes = getRepository(AntennaNote);
|
|||||||
export const PromoNotes = getRepository(PromoNote);
|
export const PromoNotes = getRepository(PromoNote);
|
||||||
export const PromoReads = getRepository(PromoRead);
|
export const PromoReads = getRepository(PromoRead);
|
||||||
export const Relays = getCustomRepository(RelayRepository);
|
export const Relays = getCustomRepository(RelayRepository);
|
||||||
|
export const MutedNotes = getRepository(MutedNote);
|
||||||
|
@ -239,6 +239,7 @@ export class UserRepository extends Repository<User> {
|
|||||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
integrations: profile!.integrations,
|
integrations: profile!.integrations,
|
||||||
|
mutedWords: profile!.mutedWords,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|
||||||
...(opts.includeSecrets ? {
|
...(opts.includeSecrets ? {
|
||||||
|
13
src/server/api/common/generate-muted-note-query.ts
Normal file
13
src/server/api/common/generate-muted-note-query.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { User } from '../../../models/entities/user';
|
||||||
|
import { MutedNotes } from '../../../models';
|
||||||
|
import { SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
|
export function generateMutedNoteQuery(q: SelectQueryBuilder<any>, me: User) {
|
||||||
|
const mutedQuery = MutedNotes.createQueryBuilder('muted')
|
||||||
|
.select('muted.noteId')
|
||||||
|
.where('muted.userId = :userId', { userId: me.id });
|
||||||
|
|
||||||
|
q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`);
|
||||||
|
|
||||||
|
q.setParameters(mutedQuery.getParameters());
|
||||||
|
}
|
@ -142,7 +142,11 @@ export const meta = {
|
|||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'ピン留めするページID'
|
'ja-JP': 'ピン留めするページID'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
|
mutedWords: {
|
||||||
|
validator: $.optional.arr($.arr($.str))
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
@ -193,6 +197,10 @@ export default define(meta, async (ps, user, token) => {
|
|||||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||||
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
|
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
|
||||||
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
|
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
|
||||||
|
if (ps.mutedWords !== undefined) {
|
||||||
|
profileUpdates.mutedWords = ps.mutedWords;
|
||||||
|
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
||||||
|
}
|
||||||
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
||||||
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
|
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
|
||||||
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
|
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
|
||||||
|
@ -10,6 +10,7 @@ import { activeUsersChart } from '../../../../services/chart';
|
|||||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||||
import { injectPromo } from '../../common/inject-promo';
|
import { injectPromo } from '../../common/inject-promo';
|
||||||
import { injectFeatured } from '../../common/inject-featured';
|
import { injectFeatured } from '../../common/inject-featured';
|
||||||
|
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
@ -83,6 +84,7 @@ export default define(meta, async (ps, user) => {
|
|||||||
|
|
||||||
generateRepliesQuery(query, user);
|
generateRepliesQuery(query, user);
|
||||||
if (user) generateMuteQuery(query, user);
|
if (user) generateMuteQuery(query, user);
|
||||||
|
if (user) generateMutedNoteQuery(query, user);
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
|
@ -12,6 +12,7 @@ import { activeUsersChart } from '../../../../services/chart';
|
|||||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||||
import { injectPromo } from '../../common/inject-promo';
|
import { injectPromo } from '../../common/inject-promo';
|
||||||
import { injectFeatured } from '../../common/inject-featured';
|
import { injectFeatured } from '../../common/inject-featured';
|
||||||
|
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
@ -133,6 +134,7 @@ export default define(meta, async (ps, user) => {
|
|||||||
generateRepliesQuery(query, user);
|
generateRepliesQuery(query, user);
|
||||||
generateVisibilityQuery(query, user);
|
generateVisibilityQuery(query, user);
|
||||||
generateMuteQuery(query, user);
|
generateMuteQuery(query, user);
|
||||||
|
generateMutedNoteQuery(query, user);
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
if (ps.includeMyRenotes === false) {
|
||||||
query.andWhere(new Brackets(qb => {
|
query.andWhere(new Brackets(qb => {
|
||||||
|
@ -12,6 +12,7 @@ import { Brackets } from 'typeorm';
|
|||||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||||
import { injectPromo } from '../../common/inject-promo';
|
import { injectPromo } from '../../common/inject-promo';
|
||||||
import { injectFeatured } from '../../common/inject-featured';
|
import { injectFeatured } from '../../common/inject-featured';
|
||||||
|
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
@ -101,6 +102,7 @@ export default define(meta, async (ps, user) => {
|
|||||||
generateRepliesQuery(query, user);
|
generateRepliesQuery(query, user);
|
||||||
generateVisibilityQuery(query, user);
|
generateVisibilityQuery(query, user);
|
||||||
if (user) generateMuteQuery(query, user);
|
if (user) generateMuteQuery(query, user);
|
||||||
|
if (user) generateMutedNoteQuery(query, user);
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
|
@ -10,6 +10,7 @@ import { Brackets } from 'typeorm';
|
|||||||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||||
import { injectPromo } from '../../common/inject-promo';
|
import { injectPromo } from '../../common/inject-promo';
|
||||||
import { injectFeatured } from '../../common/inject-featured';
|
import { injectFeatured } from '../../common/inject-featured';
|
||||||
|
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
@ -126,6 +127,7 @@ export default define(meta, async (ps, user) => {
|
|||||||
generateRepliesQuery(query, user);
|
generateRepliesQuery(query, user);
|
||||||
generateVisibilityQuery(query, user);
|
generateVisibilityQuery(query, user);
|
||||||
generateMuteQuery(query, user);
|
generateMuteQuery(query, user);
|
||||||
|
generateMutedNoteQuery(query, user);
|
||||||
|
|
||||||
if (ps.includeMyRenotes === false) {
|
if (ps.includeMyRenotes === false) {
|
||||||
query.andWhere(new Brackets(qb => {
|
query.andWhere(new Brackets(qb => {
|
||||||
|
@ -15,6 +15,10 @@ export default abstract class Channel {
|
|||||||
return this.connection.user;
|
return this.connection.user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected get userProfile() {
|
||||||
|
return this.connection.userProfile;
|
||||||
|
}
|
||||||
|
|
||||||
protected get following() {
|
protected get following() {
|
||||||
return this.connection.following;
|
return this.connection.following;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import Channel from '../channel';
|
|||||||
import { fetchMeta } from '../../../../misc/fetch-meta';
|
import { fetchMeta } from '../../../../misc/fetch-meta';
|
||||||
import { Notes } from '../../../../models';
|
import { Notes } from '../../../../models';
|
||||||
import { PackedNote } from '../../../../models/repositories/note';
|
import { PackedNote } from '../../../../models/repositories/note';
|
||||||
|
import { checkWordMute } from '../../../../misc/check-word-mute';
|
||||||
|
|
||||||
export default class extends Channel {
|
export default class extends Channel {
|
||||||
public readonly chName = 'globalTimeline';
|
public readonly chName = 'globalTimeline';
|
||||||
@ -47,6 +48,13 @@ export default class extends Channel {
|
|||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (shouldMuteThisNote(note, this.muting)) return;
|
if (shouldMuteThisNote(note, this.muting)) return;
|
||||||
|
|
||||||
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
||||||
|
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
|||||||
import Channel from '../channel';
|
import Channel from '../channel';
|
||||||
import { Notes } from '../../../../models';
|
import { Notes } from '../../../../models';
|
||||||
import { PackedNote } from '../../../../models/repositories/note';
|
import { PackedNote } from '../../../../models/repositories/note';
|
||||||
|
import { checkWordMute } from '../../../../misc/check-word-mute';
|
||||||
|
|
||||||
export default class extends Channel {
|
export default class extends Channel {
|
||||||
public readonly chName = 'homeTimeline';
|
public readonly chName = 'homeTimeline';
|
||||||
@ -52,6 +53,13 @@ export default class extends Channel {
|
|||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (shouldMuteThisNote(note, this.muting)) return;
|
if (shouldMuteThisNote(note, this.muting)) return;
|
||||||
|
|
||||||
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
||||||
|
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { fetchMeta } from '../../../../misc/fetch-meta';
|
|||||||
import { Notes } from '../../../../models';
|
import { Notes } from '../../../../models';
|
||||||
import { PackedNote } from '../../../../models/repositories/note';
|
import { PackedNote } from '../../../../models/repositories/note';
|
||||||
import { PackedUser } from '../../../../models/repositories/user';
|
import { PackedUser } from '../../../../models/repositories/user';
|
||||||
|
import { checkWordMute } from '../../../../misc/check-word-mute';
|
||||||
|
|
||||||
export default class extends Channel {
|
export default class extends Channel {
|
||||||
public readonly chName = 'hybridTimeline';
|
public readonly chName = 'hybridTimeline';
|
||||||
@ -61,6 +62,13 @@ export default class extends Channel {
|
|||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (shouldMuteThisNote(note, this.muting)) return;
|
if (shouldMuteThisNote(note, this.muting)) return;
|
||||||
|
|
||||||
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
||||||
|
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { fetchMeta } from '../../../../misc/fetch-meta';
|
|||||||
import { Notes } from '../../../../models';
|
import { Notes } from '../../../../models';
|
||||||
import { PackedNote } from '../../../../models/repositories/note';
|
import { PackedNote } from '../../../../models/repositories/note';
|
||||||
import { PackedUser } from '../../../../models/repositories/user';
|
import { PackedUser } from '../../../../models/repositories/user';
|
||||||
|
import { checkWordMute } from '../../../../misc/check-word-mute';
|
||||||
|
|
||||||
export default class extends Channel {
|
export default class extends Channel {
|
||||||
public readonly chName = 'localTimeline';
|
public readonly chName = 'localTimeline';
|
||||||
@ -49,6 +50,13 @@ export default class extends Channel {
|
|||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (shouldMuteThisNote(note, this.muting)) return;
|
if (shouldMuteThisNote(note, this.muting)) return;
|
||||||
|
|
||||||
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
|
||||||
|
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
|
||||||
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
|
||||||
|
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,15 +7,17 @@ import Channel from './channel';
|
|||||||
import channels from './channels';
|
import channels from './channels';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { User } from '../../../models/entities/user';
|
import { User } from '../../../models/entities/user';
|
||||||
import { Users, Followings, Mutings } from '../../../models';
|
import { Users, Followings, Mutings, UserProfiles } from '../../../models';
|
||||||
import { ApiError } from '../error';
|
import { ApiError } from '../error';
|
||||||
import { AccessToken } from '../../../models/entities/access-token';
|
import { AccessToken } from '../../../models/entities/access-token';
|
||||||
|
import { UserProfile } from '../../../models/entities/user-profile';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main stream connection
|
* Main stream connection
|
||||||
*/
|
*/
|
||||||
export default class Connection {
|
export default class Connection {
|
||||||
public user?: User;
|
public user?: User;
|
||||||
|
public userProfile?: UserProfile;
|
||||||
public following: User['id'][] = [];
|
public following: User['id'][] = [];
|
||||||
public muting: User['id'][] = [];
|
public muting: User['id'][] = [];
|
||||||
public token?: AccessToken;
|
public token?: AccessToken;
|
||||||
@ -25,6 +27,7 @@ export default class Connection {
|
|||||||
private subscribingNotes: any = {};
|
private subscribingNotes: any = {};
|
||||||
private followingClock: NodeJS.Timer;
|
private followingClock: NodeJS.Timer;
|
||||||
private mutingClock: NodeJS.Timer;
|
private mutingClock: NodeJS.Timer;
|
||||||
|
private userProfileClock: NodeJS.Timer;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
wsConnection: websocket.connection,
|
wsConnection: websocket.connection,
|
||||||
@ -49,6 +52,9 @@ export default class Connection {
|
|||||||
|
|
||||||
this.updateMuting();
|
this.updateMuting();
|
||||||
this.mutingClock = setInterval(this.updateMuting, 5000);
|
this.mutingClock = setInterval(this.updateMuting, 5000);
|
||||||
|
|
||||||
|
this.updateUserProfile();
|
||||||
|
this.userProfileClock = setInterval(this.updateUserProfile, 5000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,6 +268,13 @@ export default class Connection {
|
|||||||
this.muting = mutings.map(x => x.muteeId);
|
this.muting = mutings.map(x => x.muteeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
private async updateUserProfile() {
|
||||||
|
this.userProfile = await UserProfiles.findOne({
|
||||||
|
userId: this.user!.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ストリームが切れたとき
|
* ストリームが切れたとき
|
||||||
*/
|
*/
|
||||||
@ -273,5 +286,6 @@ export default class Connection {
|
|||||||
|
|
||||||
if (this.followingClock) clearInterval(this.followingClock);
|
if (this.followingClock) clearInterval(this.followingClock);
|
||||||
if (this.mutingClock) clearInterval(this.mutingClock);
|
if (this.mutingClock) clearInterval(this.mutingClock);
|
||||||
|
if (this.userProfileClock) clearInterval(this.userProfileClock);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ import extractMentions from '../../misc/extract-mentions';
|
|||||||
import extractEmojis from '../../misc/extract-emojis';
|
import extractEmojis from '../../misc/extract-emojis';
|
||||||
import extractHashtags from '../../misc/extract-hashtags';
|
import extractHashtags from '../../misc/extract-hashtags';
|
||||||
import { Note, IMentionedRemoteUsers } from '../../models/entities/note';
|
import { Note, IMentionedRemoteUsers } from '../../models/entities/note';
|
||||||
import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings } from '../../models';
|
import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes } from '../../models';
|
||||||
import { DriveFile } from '../../models/entities/drive-file';
|
import { DriveFile } from '../../models/entities/drive-file';
|
||||||
import { App } from '../../models/entities/app';
|
import { App } from '../../models/entities/app';
|
||||||
import { Not, getConnection, In } from 'typeorm';
|
import { Not, getConnection, In } from 'typeorm';
|
||||||
@ -29,6 +29,7 @@ import { createNotification } from '../create-notification';
|
|||||||
import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
|
import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error';
|
||||||
import { ensure } from '../../prelude/ensure';
|
import { ensure } from '../../prelude/ensure';
|
||||||
import { checkHitAntenna } from '../../misc/check-hit-antenna';
|
import { checkHitAntenna } from '../../misc/check-hit-antenna';
|
||||||
|
import { checkWordMute } from '../../misc/check-word-mute';
|
||||||
import { addNoteToAntenna } from '../add-note-to-antenna';
|
import { addNoteToAntenna } from '../add-note-to-antenna';
|
||||||
import { countSameRenotes } from '../../misc/count-same-renotes';
|
import { countSameRenotes } from '../../misc/count-same-renotes';
|
||||||
import { deliverToRelays } from '../relay';
|
import { deliverToRelays } from '../relay';
|
||||||
@ -219,6 +220,24 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
|
|||||||
// Increment notes count (user)
|
// Increment notes count (user)
|
||||||
incNotesCountOfUser(user);
|
incNotesCountOfUser(user);
|
||||||
|
|
||||||
|
// Word mute
|
||||||
|
UserProfiles.find({
|
||||||
|
enableWordMute: true
|
||||||
|
}).then(us => {
|
||||||
|
for (const u of us) {
|
||||||
|
checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
|
||||||
|
if (shouldMute) {
|
||||||
|
MutedNotes.save({
|
||||||
|
id: genId(),
|
||||||
|
userId: u.userId,
|
||||||
|
noteId: note.id,
|
||||||
|
reason: 'word',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Antenna
|
// Antenna
|
||||||
Antennas.find().then(async antennas => {
|
Antennas.find().then(async antennas => {
|
||||||
const followings = await Followings.createQueryBuilder('following')
|
const followings = await Followings.createQueryBuilder('following')
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
|
export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'] as const;
|
||||||
|
|
||||||
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||||
|
|
||||||
|
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
||||||
|
48
yarn.lock
48
yarn.lock
@ -3245,6 +3245,11 @@ entities@^2.0.0, entities@~2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
|
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
|
||||||
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
|
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
|
||||||
|
|
||||||
|
env-paths@^2.2.0:
|
||||||
|
version "2.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
|
||||||
|
integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==
|
||||||
|
|
||||||
errno@^0.1.3:
|
errno@^0.1.3:
|
||||||
version "0.1.7"
|
version "0.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
|
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
|
||||||
@ -4129,6 +4134,11 @@ graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, g
|
|||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
|
||||||
integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
|
integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
|
||||||
|
|
||||||
|
graceful-fs@^4.2.3:
|
||||||
|
version "4.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||||
|
integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==
|
||||||
|
|
||||||
growl@1.10.5:
|
growl@1.10.5:
|
||||||
version "1.10.5"
|
version "1.10.5"
|
||||||
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
|
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
|
||||||
@ -4658,6 +4668,11 @@ insert-text-at-cursor@0.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/insert-text-at-cursor/-/insert-text-at-cursor-0.3.0.tgz#1819607680ec1570618347c4cd475e791faa25da"
|
resolved "https://registry.yarnpkg.com/insert-text-at-cursor/-/insert-text-at-cursor-0.3.0.tgz#1819607680ec1570618347c4cd475e791faa25da"
|
||||||
integrity sha512-/nPtyeX9xPUvxZf+r0518B7uqNKlP+LqNJqSiXFEaa2T71rWIwTVXGH7hB9xO/EVdwa5/pWlFCPwShOW81XIxQ==
|
integrity sha512-/nPtyeX9xPUvxZf+r0518B7uqNKlP+LqNJqSiXFEaa2T71rWIwTVXGH7hB9xO/EVdwa5/pWlFCPwShOW81XIxQ==
|
||||||
|
|
||||||
|
install-artifact-from-github@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.0.2.tgz#e1e478dd29880b9112ecd684a84029603e234a9d"
|
||||||
|
integrity sha512-yuMFBSVIP3vD0SDBGUqeIpgOAIlFx8eQFknQObpkYEM5gsl9hy6R9Ms3aV+Vw9MMyYsoPMeex0XDnfgY7uzc+Q==
|
||||||
|
|
||||||
interpret@^1.1.0:
|
interpret@^1.1.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
|
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
|
||||||
@ -6187,7 +6202,7 @@ mz@^2.4.0, mz@^2.7.0:
|
|||||||
object-assign "^4.0.1"
|
object-assign "^4.0.1"
|
||||||
thenify-all "^1.0.0"
|
thenify-all "^1.0.0"
|
||||||
|
|
||||||
nan@^2.14.0:
|
nan@^2.14.0, nan@^2.14.1:
|
||||||
version "2.14.1"
|
version "2.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
|
||||||
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
|
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
|
||||||
@ -6283,6 +6298,22 @@ node-forge@^0.9.1:
|
|||||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
|
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
|
||||||
integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==
|
integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==
|
||||||
|
|
||||||
|
node-gyp@^7.0.0:
|
||||||
|
version "7.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.0.0.tgz#2e88425ce84e9b1a4433958ed55d74c70fffb6be"
|
||||||
|
integrity sha512-ZW34qA3CJSPKDz2SJBHKRvyNQN0yWO5EGKKksJc+jElu9VA468gwJTyTArC1iOXU7rN3Wtfg/CMt/dBAOFIjvg==
|
||||||
|
dependencies:
|
||||||
|
env-paths "^2.2.0"
|
||||||
|
glob "^7.1.4"
|
||||||
|
graceful-fs "^4.2.3"
|
||||||
|
nopt "^4.0.3"
|
||||||
|
npmlog "^4.1.2"
|
||||||
|
request "^2.88.2"
|
||||||
|
rimraf "^2.6.3"
|
||||||
|
semver "^7.3.2"
|
||||||
|
tar "^6.0.1"
|
||||||
|
which "^2.0.2"
|
||||||
|
|
||||||
node-object-hash@^1.2.0:
|
node-object-hash@^1.2.0:
|
||||||
version "1.4.2"
|
version "1.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94"
|
resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-1.4.2.tgz#385833d85b229902b75826224f6077be969a9e94"
|
||||||
@ -7775,6 +7806,15 @@ rdf-canonize@^1.0.2:
|
|||||||
node-forge "^0.9.1"
|
node-forge "^0.9.1"
|
||||||
semver "^6.3.0"
|
semver "^6.3.0"
|
||||||
|
|
||||||
|
re2@1.15.4:
|
||||||
|
version "1.15.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/re2/-/re2-1.15.4.tgz#2ffc3e4894fb60430393459978197648be01a0a9"
|
||||||
|
integrity sha512-7w3K+Daq/JjbX/dz5voMt7B9wlprVBQnMiypyCojAZ99kcAL+3LiJ5uBoX/u47l8eFTVq3Wj+V0pmvU+CT8tOg==
|
||||||
|
dependencies:
|
||||||
|
install-artifact-from-github "^1.0.2"
|
||||||
|
nan "^2.14.1"
|
||||||
|
node-gyp "^7.0.0"
|
||||||
|
|
||||||
read-pkg-up@^1.0.1:
|
read-pkg-up@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
|
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
|
||||||
@ -8183,7 +8223,7 @@ rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
rimraf@^2.6.2:
|
rimraf@^2.6.2, rimraf@^2.6.3:
|
||||||
version "2.7.1"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||||
@ -9088,7 +9128,7 @@ tar-stream@^2.0.0:
|
|||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
readable-stream "^3.1.1"
|
readable-stream "^3.1.1"
|
||||||
|
|
||||||
tar@^6.0.2:
|
tar@^6.0.1, tar@^6.0.2:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39"
|
resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39"
|
||||||
integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==
|
integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==
|
||||||
@ -10138,7 +10178,7 @@ which-pm-runs@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
|
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
|
||||||
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
|
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
|
||||||
|
|
||||||
which@2.0.2, which@^2.0.1:
|
which@2.0.2, which@^2.0.1, which@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||||
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
|
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
|
||||||
|
Loading…
Reference in New Issue
Block a user