parent
							
								
									b5a1fdd4c7
								
							
						
					
					
						commit
						cf43dd6ec5
					
				
					 32 changed files with 485 additions and 12 deletions
				
			
		|  | @ -553,6 +553,17 @@ emptyToDisableSmtpAuth: "ユーザー名とパスワードを空欄にするこ | |||
| smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する" | ||||
| smtpSecureInfo: "STARTTLS使用時はオフにします。" | ||||
| testEmail: "配信テスト" | ||||
| wordMute: "ワードミュート" | ||||
| userSaysSomething: "{name}が何かを言いました" | ||||
| 
 | ||||
| _wordMute: | ||||
|   muteWords: "ミュートするワード" | ||||
|   muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。" | ||||
|   muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。" | ||||
|   softDescription: "指定した条件のノートをタイムラインから隠します。" | ||||
|   hardDescription: "指定した条件のノートをタイムラインに追加しないようにします。追加されなかったノートは、条件を変更しても除外されたままになります。" | ||||
|   soft: "ソフト" | ||||
|   hard: "ハード" | ||||
| 
 | ||||
| _theme: | ||||
|   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", | ||||
| 		"randomcolor": "0.5.4", | ||||
| 		"ratelimiter": "3.4.1", | ||||
| 		"re2": "1.15.4", | ||||
| 		"recaptcha-promise": "0.1.3", | ||||
| 		"reconnecting-websocket": "4.4.0", | ||||
| 		"redis": "3.0.2", | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
							
								
								
									
										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" | ||||
|   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: | ||||
|   version "0.1.7" | ||||
|   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" | ||||
|   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: | ||||
|   version "1.10.5" | ||||
|   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" | ||||
|   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: | ||||
|   version "1.2.0" | ||||
|   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" | ||||
|     thenify-all "^1.0.0" | ||||
| 
 | ||||
| nan@^2.14.0: | ||||
| nan@^2.14.0, nan@^2.14.1: | ||||
|   version "2.14.1" | ||||
|   resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" | ||||
|   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" | ||||
|   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: | ||||
|   version "1.4.2" | ||||
|   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" | ||||
|     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: | ||||
|   version "1.0.1" | ||||
|   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: | ||||
|     glob "^7.1.3" | ||||
| 
 | ||||
| rimraf@^2.6.2: | ||||
| rimraf@^2.6.2, rimraf@^2.6.3: | ||||
|   version "2.7.1" | ||||
|   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" | ||||
|   integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== | ||||
|  | @ -9088,7 +9128,7 @@ tar-stream@^2.0.0: | |||
|     inherits "^2.0.3" | ||||
|     readable-stream "^3.1.1" | ||||
| 
 | ||||
| tar@^6.0.2: | ||||
| tar@^6.0.1, tar@^6.0.2: | ||||
|   version "6.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39" | ||||
|   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" | ||||
|   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" | ||||
|   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" | ||||
|   integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue