parent
b5a1fdd4c7
commit
cf43dd6ec5
32 changed files with 485 additions and 12 deletions
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="note _panel"
|
||||
v-if="!muted"
|
||||
v-show="!isDeleted"
|
||||
:tabindex="!isDeleted ? '-1' : null"
|
||||
:class="{ renote: isRenote }"
|
||||
|
@ -84,6 +85,13 @@
|
|||
</article>
|
||||
<x-sub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
|
||||
</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>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -105,6 +113,7 @@ import pleaseLogin from '../scripts/please-login';
|
|||
import { focusPrev, focusNext } from '../scripts/focus';
|
||||
import { url } from '../config';
|
||||
import copyToClipboard from '../scripts/copy-to-clipboard';
|
||||
import { checkWordMute } from '../scripts/check-word-mute';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
|
@ -142,6 +151,7 @@ export default Vue.extend({
|
|||
replies: [],
|
||||
showContent: false,
|
||||
isDeleted: false,
|
||||
muted: false,
|
||||
myReaction: null,
|
||||
reactions: {},
|
||||
emojis: [],
|
||||
|
@ -227,15 +237,16 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.emojis = [...this.appearNote.emojis];
|
||||
this.reactions = { ...this.appearNote.reactions };
|
||||
this.myReaction = this.appearNote.myReaction;
|
||||
|
||||
async created() {
|
||||
if (this.$store.getters.isSignedIn) {
|
||||
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) {
|
||||
this.$root.api('notes/children', {
|
||||
noteId: this.appearNote.id,
|
||||
|
@ -976,4 +987,10 @@ export default Vue.extend({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.muted {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</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-drive/>
|
||||
<x-mute-block/>
|
||||
<x-word-mute/>
|
||||
<x-security/>
|
||||
<x-2fa/>
|
||||
<x-integration/>
|
||||
|
@ -47,6 +48,7 @@ import XImportExport from './import-export.vue';
|
|||
import XDrive from './drive.vue';
|
||||
import XReactionSetting from './reaction.vue';
|
||||
import XMuteBlock from './mute-block.vue';
|
||||
import XWordMute from './word-mute.vue';
|
||||
import XSecurity from './security.vue';
|
||||
import X2fa from './2fa.vue';
|
||||
import XIntegration from './integration.vue';
|
||||
|
@ -68,6 +70,7 @@ export default Vue.extend({
|
|||
XDrive,
|
||||
XReactionSetting,
|
||||
XMuteBlock,
|
||||
XWordMute,
|
||||
XSecurity,
|
||||
X2fa,
|
||||
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}}]',
|
||||
memo: null,
|
||||
reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
|
||||
mutedWords: [],
|
||||
};
|
||||
|
||||
export const defaultDeviceUserSettings = {
|
||||
|
|
|
@ -355,6 +355,10 @@ hr {
|
|||
padding: 16px;
|
||||
}
|
||||
|
||||
&._noPad {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
& + ._content {
|
||||
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 { program } from '../argv';
|
||||
import { Relay } from '../models/entities/relay';
|
||||
import { MutedNote } from '../models/entities/muted-note';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||
|
||||
|
@ -151,6 +152,7 @@ export const entities = [
|
|||
ReversiGame,
|
||||
ReversiMatching,
|
||||
Relay,
|
||||
MutedNote,
|
||||
...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>;
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableWordMute: boolean;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: []
|
||||
})
|
||||
public mutedWords: string[][];
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
|
|
|
@ -53,6 +53,7 @@ import { PromoNote } from './entities/promo-note';
|
|||
import { PromoRead } from './entities/promo-read';
|
||||
import { EmojiRepository } from './repositories/emoji';
|
||||
import { RelayRepository } from './repositories/relay';
|
||||
import { MutedNote } from './entities/muted-note';
|
||||
|
||||
export const Announcements = getRepository(Announcement);
|
||||
export const AnnouncementReads = getRepository(AnnouncementRead);
|
||||
|
@ -108,3 +109,4 @@ export const AntennaNotes = getRepository(AntennaNote);
|
|||
export const PromoNotes = getRepository(PromoNote);
|
||||
export const PromoReads = getRepository(PromoRead);
|
||||
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),
|
||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||
integrations: profile!.integrations,
|
||||
mutedWords: profile!.mutedWords,
|
||||
} : {}),
|
||||
|
||||
...(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: {
|
||||
'ja-JP': 'ピン留めするページID'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mutedWords: {
|
||||
validator: $.optional.arr($.arr($.str))
|
||||
},
|
||||
},
|
||||
|
||||
errors: {
|
||||
|
@ -193,6 +197,10 @@ export default define(meta, async (ps, user, token) => {
|
|||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
|
||||
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.isBot === 'boolean') updates.isBot = ps.isBot;
|
||||
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 { injectPromo } from '../../common/inject-promo';
|
||||
import { injectFeatured } from '../../common/inject-featured';
|
||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -83,6 +84,7 @@ export default define(meta, async (ps, user) => {
|
|||
|
||||
generateRepliesQuery(query, user);
|
||||
if (user) generateMuteQuery(query, user);
|
||||
if (user) generateMutedNoteQuery(query, user);
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
|
|
|
@ -12,6 +12,7 @@ import { activeUsersChart } from '../../../../services/chart';
|
|||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||
import { injectPromo } from '../../common/inject-promo';
|
||||
import { injectFeatured } from '../../common/inject-featured';
|
||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -133,6 +134,7 @@ export default define(meta, async (ps, user) => {
|
|||
generateRepliesQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMuteQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Brackets } from 'typeorm';
|
|||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||
import { injectPromo } from '../../common/inject-promo';
|
||||
import { injectFeatured } from '../../common/inject-featured';
|
||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -101,6 +102,7 @@ export default define(meta, async (ps, user) => {
|
|||
generateRepliesQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
if (user) generateMuteQuery(query, user);
|
||||
if (user) generateMutedNoteQuery(query, user);
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Brackets } from 'typeorm';
|
|||
import { generateRepliesQuery } from '../../common/generate-replies-query';
|
||||
import { injectPromo } from '../../common/inject-promo';
|
||||
import { injectFeatured } from '../../common/inject-featured';
|
||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -126,6 +127,7 @@ export default define(meta, async (ps, user) => {
|
|||
generateRepliesQuery(query, user);
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMuteQuery(query, user);
|
||||
generateMutedNoteQuery(query, user);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
|
|
|
@ -15,6 +15,10 @@ export default abstract class Channel {
|
|||
return this.connection.user;
|
||||
}
|
||||
|
||||
protected get userProfile() {
|
||||
return this.connection.userProfile;
|
||||
}
|
||||
|
||||
protected get following() {
|
||||
return this.connection.following;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import Channel from '../channel';
|
|||
import { fetchMeta } from '../../../../misc/fetch-meta';
|
||||
import { Notes } from '../../../../models';
|
||||
import { PackedNote } from '../../../../models/repositories/note';
|
||||
import { checkWordMute } from '../../../../misc/check-word-mute';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'globalTimeline';
|
||||
|
@ -47,6 +48,13 @@ export default class extends Channel {
|
|||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import shouldMuteThisNote from '../../../../misc/should-mute-this-note';
|
|||
import Channel from '../channel';
|
||||
import { Notes } from '../../../../models';
|
||||
import { PackedNote } from '../../../../models/repositories/note';
|
||||
import { checkWordMute } from '../../../../misc/check-word-mute';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'homeTimeline';
|
||||
|
@ -52,6 +53,13 @@ export default class extends Channel {
|
|||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { fetchMeta } from '../../../../misc/fetch-meta';
|
|||
import { Notes } from '../../../../models';
|
||||
import { PackedNote } from '../../../../models/repositories/note';
|
||||
import { PackedUser } from '../../../../models/repositories/user';
|
||||
import { checkWordMute } from '../../../../misc/check-word-mute';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'hybridTimeline';
|
||||
|
@ -61,6 +62,13 @@ export default class extends Channel {
|
|||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { fetchMeta } from '../../../../misc/fetch-meta';
|
|||
import { Notes } from '../../../../models';
|
||||
import { PackedNote } from '../../../../models/repositories/note';
|
||||
import { PackedUser } from '../../../../models/repositories/user';
|
||||
import { checkWordMute } from '../../../../misc/check-word-mute';
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'localTimeline';
|
||||
|
@ -49,6 +50,13 @@ export default class extends Channel {
|
|||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,15 +7,17 @@ import Channel from './channel';
|
|||
import channels from './channels';
|
||||
import { EventEmitter } from 'events';
|
||||
import { User } from '../../../models/entities/user';
|
||||
import { Users, Followings, Mutings } from '../../../models';
|
||||
import { Users, Followings, Mutings, UserProfiles } from '../../../models';
|
||||
import { ApiError } from '../error';
|
||||
import { AccessToken } from '../../../models/entities/access-token';
|
||||
import { UserProfile } from '../../../models/entities/user-profile';
|
||||
|
||||
/**
|
||||
* Main stream connection
|
||||
*/
|
||||
export default class Connection {
|
||||
public user?: User;
|
||||
public userProfile?: UserProfile;
|
||||
public following: User['id'][] = [];
|
||||
public muting: User['id'][] = [];
|
||||
public token?: AccessToken;
|
||||
|
@ -25,6 +27,7 @@ export default class Connection {
|
|||
private subscribingNotes: any = {};
|
||||
private followingClock: NodeJS.Timer;
|
||||
private mutingClock: NodeJS.Timer;
|
||||
private userProfileClock: NodeJS.Timer;
|
||||
|
||||
constructor(
|
||||
wsConnection: websocket.connection,
|
||||
|
@ -49,6 +52,9 @@ export default class Connection {
|
|||
|
||||
this.updateMuting();
|
||||
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);
|
||||
}
|
||||
|
||||
@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.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 extractHashtags from '../../misc/extract-hashtags';
|
||||
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 { App } from '../../models/entities/app';
|
||||
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 { ensure } from '../../prelude/ensure';
|
||||
import { checkHitAntenna } from '../../misc/check-hit-antenna';
|
||||
import { checkWordMute } from '../../misc/check-word-mute';
|
||||
import { addNoteToAntenna } from '../add-note-to-antenna';
|
||||
import { countSameRenotes } from '../../misc/count-same-renotes';
|
||||
import { deliverToRelays } from '../relay';
|
||||
|
@ -219,6 +220,24 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
|
|||
// Increment notes count (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
|
||||
Antennas.find().then(async antennas => {
|
||||
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 noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
|
||||
|
||||
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue