diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 874e97f3a4..1eac2cddbc 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -24,6 +24,7 @@ import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mainRouter } from '@/router/main.js'; import { type Keymap, makeHotkey } from '@/scripts/hotkey.js'; import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js'; +import { initMuteInfo } from '@/scripts/check-word-mute.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => { @@ -343,11 +344,14 @@ export async function mainBoot() { } } + initMuteInfo(); + const main = markRaw(stream.useChannel('main', null, 'System')); // 自分の情報が更新されたとき main.on('meUpdated', i => { updateAccountPartial(i); + initMuteInfo(); }); main.on('readAllNotifications', () => { diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index a23ff9b48e..c77c83b044 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -280,8 +280,8 @@ const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filte const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); const collapsed = ref(appearNote.value.cw == null && isLong); const isDeleted = ref(false); -const muted = ref(checkMute(appearNote.value, $i?.mutedWords)); -const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true)); +const muted = ref(checkMute(appearNote.value, 'soft')); +const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, 'hard', true)); const showSoftWordMutedWord = computed(() => defaultStore.state.showSoftWordMutedWord); const translation = ref(null); const translating = ref(false); @@ -300,20 +300,18 @@ const pleaseLoginContext = computed(() => ({ })); /* Overload FunctionにLintが対応していないのでコメントアウト -function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array | undefined | null, checkOnly: true): boolean; -function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array | undefined | null, checkOnly: false): Array | false | 'sensitiveMute'; +function checkMute(noteToCheck: Misskey.entities.Note, type: 'soft' | 'hard', checkOnly: true): boolean; +function checkMute(noteToCheck: Misskey.entities.Note, type: 'soft' | 'hard', checkOnly: false): Array | false | 'sensitiveMute'; */ -function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array | undefined | null, checkOnly = false): Array | false | 'sensitiveMute' { - if (mutedWords != null) { - const result = checkWordMute(noteToCheck, $i, mutedWords); - if (Array.isArray(result)) return result; +function checkMute(noteToCheck: Misskey.entities.Note, type: 'soft' | 'hard', checkOnly = false): Array | false | 'sensitiveMute' { + const result = checkWordMute(noteToCheck, $i, type); + if (Array.isArray(result)) return result; - const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords); - if (Array.isArray(replyResult)) return replyResult; + const replyResult = noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, type); + if (Array.isArray(replyResult)) return replyResult; - const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords); - if (Array.isArray(renoteResult)) return renoteResult; - } + const renoteResult = noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, type); + if (Array.isArray(renoteResult)) return renoteResult; if (checkOnly) return false; diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 9d3374d433..d1de6a8850 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -296,7 +296,7 @@ const galleryEl = shallowRef>(); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(false); const isDeleted = ref(false); -const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : false); +const muted = ref($i ? checkWordMute(appearNote.value, $i, 'soft') : false); const translation = ref(null); const translating = ref(false); const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null; diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index e4bade309b..2070a44e5a 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -62,7 +62,7 @@ const props = withDefaults(defineProps<{ depth: 1, }); -const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false); +const muted = ref($i ? checkWordMute(props.note, $i, 'soft') : false); const showContent = ref(false); const replies = ref([]); diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts index b54a0ef860..9f5765cda1 100644 --- a/packages/frontend/src/scripts/check-word-mute.ts +++ b/packages/frontend/src/scripts/check-word-mute.ts @@ -3,22 +3,27 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import * as AhoCorasick from 'modern-ahocorasick'; +import { c } from 'node_modules/vite/dist/node/types.d-aGj9QkWt'; import type * as Misskey from 'misskey-js'; -export function checkWordMute( - note: Misskey.entities.Note, - me: Misskey.entities.UserLite | null | undefined, - mutedWords: Array, -): Array | false { - // 自分自身の投稿は対象外 - if (me && (note.userId === me.id)) return false; +import { $i } from '@/account.js'; + +type WordMuteInfo = false | { + normals: string[]; + and: string[][]; + regex: Array<{ original: string; regex: RegExp }>; + ahoCorasick: AhoCorasick.default; +} + +type GlobalMisskeyWordMute = { + soft: WordMuteInfo; + hard: WordMuteInfo; +}; + +function createWordMuteInfo(mutedWords: Array) : WordMuteInfo { if (mutedWords.length <= 0) return false; - - const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim(); - if (text === '') return false; - const normalTexts: string[] = []; const andTexts: string[][] = []; - const regexTexts: Array<{ originaL: string; regex: RegExp }> = []; + const regexTexts: Array<{ original: string; regex: RegExp }> = []; for (const filter of mutedWords) { if (Array.isArray(filter)) { @@ -31,7 +36,7 @@ export function checkWordMute( const regExp = filter.match(/^\/(.+)\/(.*)$/); if (!regExp) continue; try { - regexTexts.push({ originaL: filter, regex: new RegExp(filter.slice(1, -1)) }); + regexTexts.push({ original: filter, regex: new RegExp(filter.slice(1, -1)) }); } catch { // 無効な正規表現はスキップ } @@ -39,17 +44,60 @@ export function checkWordMute( normalTexts.push(filter); } } - // normal wordmute with AhoCorasick + const ac = new AhoCorasick.default(normalTexts); - const normalMatches = ac.search(text); + + return { + normals: normalTexts, + and: andTexts, + regex: regexTexts, + ahoCorasick: ac, + }; +} + +function setWordMuteInfo(mutedWords: Array, hardMutedWords: Array): void { + const soft = createWordMuteInfo(mutedWords); + const hard = createWordMuteInfo(hardMutedWords); + + globalThis._misskeyWordMute = { soft, hard }; +} + +function getWordMuteInfo(): GlobalMisskeyWordMute | undefined { + if (!globalThis._misskeyWordMute) return undefined; + return globalThis._misskeyWordMute as unknown as GlobalMisskeyWordMute; +} + +export function initMuteInfo(): void { + const mutedWords = $i?.mutedWords ?? []; + const hardMutedWords = $i?.hardMutedWords ?? []; + + setWordMuteInfo(mutedWords, hardMutedWords); +} + +export function checkWordMute( + note: Misskey.entities.Note, + me: Misskey.entities.UserLite | null | undefined, + type: 'soft' | 'hard', +): Array | false { + // 自分自身の投稿は対象外 + if (me && (note.userId === me.id)) return false; + + const wordMuteInfo = getWordMuteInfo()?.[type]; + + if (wordMuteInfo == null || wordMuteInfo === false) return false; + + const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim(); + if (text === '') return false; + + const normalMatches = wordMuteInfo.ahoCorasick.search(text); // andTexts - const andMatches = andTexts.filter(texts => texts.filter(keyword => keyword !== '').every(keyword => text.includes(keyword))); + const andMatches = wordMuteInfo.and.filter(texts => texts.filter(keyword => keyword !== '').every(keyword => text.includes(keyword))); // RegExp - const regexMatches = regexTexts.filter(({ regex }) => regex.test(text)); + const regexMatches = wordMuteInfo.regex.filter(({ regex }) => regex.test(text)); - const matched: Array = normalMatches.map(match => match[1]).concat(andMatches, regexMatches.map(({ originaL }) => originaL)); + const matched: Array = normalMatches.map(match => match[1]).concat(andMatches, regexMatches.map(({ original }) => original)); return matched.length > 0 ? matched : false; }