refactor (backend): separate muted words and muted patterns

Co-authored-by: sup39 <dev@sup39.dev>
This commit is contained in:
naskya 2024-03-01 22:52:21 +09:00
parent b30e68c98c
commit d70d0f4232
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
21 changed files with 319 additions and 97 deletions

View file

@ -5,6 +5,7 @@ Critical security updates are indicated by the :warning: icon.
## Unreleased ## Unreleased
- Introduce new full-text search engine and post search filters - Introduce new full-text search engine and post search filters
- Refactoring
## v20240301 ## v20240301

View file

@ -1,6 +1,7 @@
BEGIN; BEGIN;
DELETE FROM "migrations" WHERE name IN ( DELETE FROM "migrations" WHERE name IN (
'SeparateHardMuteWordsAndPatterns1706413792769',
'IndexAltTextAndCw1708872574733', 'IndexAltTextAndCw1708872574733',
'Pgroonga1698420787202', 'Pgroonga1698420787202',
'ChangeDefaultConfigs1709251460718', 'ChangeDefaultConfigs1709251460718',
@ -14,6 +15,10 @@ DELETE FROM "migrations" WHERE name IN (
'RemoveNativeUtilsMigration1705877093218' 'RemoveNativeUtilsMigration1705877093218'
); );
-- separate-hard-mute-words-and-patterns
UPDATE "user_profile" SET "mutedWords" = "mutedWords" || array_to_json("mutedPatterns")::jsonb;
ALTER TABLE "user_profile" DROP "mutedPatterns";
-- index-alt-text-and-cw -- index-alt-text-and-cw
DROP INDEX "IDX_f4f7b93d05958527300d79ac82"; DROP INDEX "IDX_f4f7b93d05958527300d79ac82";
DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"; DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f";

View file

@ -1429,6 +1429,7 @@ _menuDisplay:
hide: "Hide" hide: "Hide"
_wordMute: _wordMute:
muteWords: "Muted words" muteWords: "Muted words"
mutePatterns: "Muted patterns"
muteLangs: "Muted Languages" muteLangs: "Muted Languages"
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks muteWordsDescription: "Separate with spaces for an AND condition or with line breaks
for an OR condition." for an OR condition."

View file

@ -1220,7 +1220,8 @@ _menuDisplay:
hide: "隠す" hide: "隠す"
_wordMute: _wordMute:
muteWords: "ミュートするワード" muteWords: "ミュートするワード"
muteLangs: "ミュートされた言語" mutePatterns: "ミュートするパターン"
muteLangs: "ミュートする言語"
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。" muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。" muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
muteLangsDescription: "OR 条件の場合はスペースまたは改行で区切ります。" muteLangsDescription: "OR 条件の場合はスペースまたは改行で区切ります。"

View file

@ -71,6 +71,8 @@ pub struct Model {
pub prevent_ai_learning: bool, pub prevent_ai_learning: bool,
#[sea_orm(column_name = "isIndexable")] #[sea_orm(column_name = "isIndexable")]
pub is_indexable: bool, pub is_indexable: bool,
#[sea_orm(column_name = "mutedPatterns")]
pub muted_patterns: Vec<String>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -0,0 +1,32 @@
export class SeparateHardMuteWordsAndPatterns1706413792769 {
name = "SeparateHardMuteWordsAndPatterns1706413792769";
async up(queryRunner) {
await queryRunner.query(
`ALTER TABLE "user_profile" ADD "mutedPatterns" text[] DEFAULT '{}'`,
);
await queryRunner.query(`
UPDATE "user_profile" SET
"mutedPatterns" = ARRAY(
SELECT jsonb_array_elements_text(jsonb_path_query_array(
"mutedWords",
'$ ? (@.type() == "string")'
))
),
"mutedWords" = jsonb_path_query_array(
"mutedWords",
'$ ? (@.type() == "array")'
)
`);
await queryRunner.query(
`ALTER TABLE "user_profile" ALTER "mutedPatterns" SET NOT NULL`,
);
}
async down(queryRunner) {
await queryRunner.query(
`UPDATE "user_profile" SET "mutedWords" = "mutedWords" || array_to_json("mutedPatterns")::jsonb`,
);
await queryRunner.query(`ALTER TABLE "user_profile" DROP "mutedPatterns"`);
}
}

View file

@ -1,6 +1,7 @@
import type { Antenna } from "@/models/entities/antenna.js"; import type { Antenna } from "@/models/entities/antenna.js";
import type { Note } from "@/models/entities/note.js"; import type { Note } from "@/models/entities/note.js";
import type { User } from "@/models/entities/user.js"; import type { User } from "@/models/entities/user.js";
import type { UserProfile } from "@/models/entities/user-profile.js";
import { Blockings, UserProfiles } from "@/models/index.js"; import { Blockings, UserProfiles } from "@/models/index.js";
import { getFullApAccount } from "@/misc/convert-host.js"; import { getFullApAccount } from "@/misc/convert-host.js";
import * as Acct from "@/misc/acct.js"; import * as Acct from "@/misc/acct.js";
@ -9,7 +10,11 @@ import { Cache } from "@/misc/cache.js";
import { getWordHardMute } from "@/misc/check-word-mute.js"; import { getWordHardMute } from "@/misc/check-word-mute.js";
const blockingCache = new Cache<User["id"][]>("blocking", 60 * 5); const blockingCache = new Cache<User["id"][]>("blocking", 60 * 5);
const mutedWordsCache = new Cache<string[][] | undefined>("mutedWords", 60 * 5); const hardMutesCache = new Cache<{
userId: UserProfile["userId"];
mutedWords: UserProfile["mutedWords"];
mutedPatterns: UserProfile["mutedPatterns"];
}>("hardMutes", 60 * 5);
export async function checkHitAntenna( export async function checkHitAntenna(
antenna: Antenna, antenna: Antenna,
@ -89,12 +94,24 @@ export async function checkHitAntenna(
); );
if (blockings.includes(antenna.userId)) return false; if (blockings.includes(antenna.userId)) return false;
const mutedWords = await mutedWordsCache.fetch(antenna.userId, () => const mutes = await hardMutesCache.fetch(antenna.userId, () =>
UserProfiles.findOneBy({ userId: antenna.userId }).then( UserProfiles.findOneByOrFail({
(profile) => profile?.mutedWords, userId: antenna.userId,
), }).then((profile) => {
return {
userId: antenna.userId,
mutedWords: profile.mutedWords,
mutedPatterns: profile.mutedPatterns,
};
}),
); );
if (await getWordHardMute(note, antenna.userId, mutedWords)) return false; if (
mutes.mutedWords != null &&
mutes.mutedPatterns != null &&
antenna.userId !== note.userId &&
(await getWordHardMute(note, mutes.mutedWords, mutes.mutedPatterns))
)
return false;
// TODO: eval expression // TODO: eval expression

View file

@ -6,11 +6,14 @@ type NoteLike = {
text: Note["text"]; text: Note["text"];
files?: Note["files"]; files?: Note["files"];
cw?: Note["cw"]; cw?: Note["cw"];
reply?: NoteLike | null;
renote?: NoteLike | null;
}; };
function checkWordMute( function checkWordMute(
note: NoteLike, note: NoteLike | null | undefined,
mutedWords: Array<string | string[]>, mutedWords: string[][],
mutedPatterns: string[],
): boolean { ): boolean {
if (note == null) return false; if (note == null) return false;
@ -21,10 +24,9 @@ function checkWordMute(
if (text === "") return false; if (text === "") return false;
for (const mutePattern of mutedWords) { for (const mutedWord of mutedWords) {
if (Array.isArray(mutePattern)) {
// Clean up // Clean up
const keywords = mutePattern.filter((keyword) => keyword !== ""); const keywords = mutedWord.filter((keyword) => keyword !== "");
if ( if (
keywords.length > 0 && keywords.length > 0 &&
@ -33,13 +35,15 @@ function checkWordMute(
) )
) )
return true; return true;
} else { }
for (const mutedPattern of mutedPatterns) {
// represents RegExp // represents RegExp
const regexp = mutePattern.match(/^\/(.+)\/(.*)$/); const regexp = mutedPattern.match(/^\/(.+)\/(.*)$/);
// This should never happen due to input sanitisation. // This should never happen due to input sanitisation.
if (!regexp) { if (!regexp) {
console.warn(`Found invalid regex in word mutes: ${mutePattern}`); console.warn(`Found invalid regex in word mutes: ${mutedPattern}`);
continue; continue;
} }
@ -49,23 +53,22 @@ function checkWordMute(
// This should never happen due to input sanitisation. // This should never happen due to input sanitisation.
} }
} }
}
return false; return false;
} }
export async function getWordHardMute( export async function getWordHardMute(
note: NoteLike, note: NoteLike | null,
meId: string | null | undefined, mutedWords: string[][],
mutedWords?: Array<string | string[]>, mutedPatterns: string[],
): Promise<boolean> { ): Promise<boolean> {
if (note.userId === meId || mutedWords == null) return false; if (note == null || mutedWords == null || mutedPatterns == null) return false;
if (mutedWords.length > 0) { if (mutedWords.length > 0) {
return ( return (
checkWordMute(note, mutedWords) || checkWordMute(note, mutedWords, mutedPatterns) ||
checkWordMute(note.reply, mutedWords) || checkWordMute(note.reply, mutedWords, mutedPatterns) ||
checkWordMute(note.renote, mutedWords) checkWordMute(note.renote, mutedWords, mutedPatterns)
); );
} }

View file

@ -217,6 +217,12 @@ export class UserProfile {
}) })
public mutedWords: string[][]; public mutedWords: string[][];
@Column("text", {
array: true,
nullable: false,
})
public mutedPatterns: string[];
@Column("jsonb", { @Column("jsonb", {
default: [], default: [],
comment: "List of instances muted by the user.", comment: "List of instances muted by the user.",

View file

@ -544,13 +544,13 @@ export const UserRepository = db.getRepository(User).extend({
? { ? {
avatarId: user.avatarId, avatarId: user.avatarId,
bannerId: user.bannerId, bannerId: user.bannerId,
injectFeaturedNote: profile!.injectFeaturedNote, injectFeaturedNote: profile?.injectFeaturedNote,
receiveAnnouncementEmail: profile!.receiveAnnouncementEmail, receiveAnnouncementEmail: profile?.receiveAnnouncementEmail,
alwaysMarkNsfw: profile!.alwaysMarkNsfw, alwaysMarkNsfw: profile?.alwaysMarkNsfw,
carefulBot: profile!.carefulBot, carefulBot: profile?.carefulBot,
autoAcceptFollowed: profile!.autoAcceptFollowed, autoAcceptFollowed: profile?.autoAcceptFollowed,
noCrawle: profile!.noCrawle, noCrawle: profile?.noCrawle,
preventAiLearning: profile!.preventAiLearning, preventAiLearning: profile?.preventAiLearning,
isExplorable: user.isExplorable, isExplorable: user.isExplorable,
isDeleted: user.isDeleted, isDeleted: user.isDeleted,
hideOnlineStatus: user.hideOnlineStatus, hideOnlineStatus: user.hideOnlineStatus,
@ -571,17 +571,18 @@ export const UserRepository = db.getRepository(User).extend({
hasUnreadNotification: this.getHasUnreadNotification(user.id), hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: hasPendingReceivedFollowRequest:
this.getHasPendingReceivedFollowRequest(user.id), this.getHasPendingReceivedFollowRequest(user.id),
mutedWords: profile!.mutedWords, mutedWords: profile?.mutedWords,
mutedInstances: profile!.mutedInstances, mutedPatterns: profile?.mutedPatterns,
mutingNotificationTypes: profile!.mutingNotificationTypes, mutedInstances: profile?.mutedInstances,
emailNotificationTypes: profile!.emailNotificationTypes, mutingNotificationTypes: profile?.mutingNotificationTypes,
emailNotificationTypes: profile?.emailNotificationTypes,
} }
: {}), : {}),
...(opts.includeSecrets ...(opts.includeSecrets
? { ? {
email: profile!.email, email: profile?.email,
emailVerified: profile!.emailVerified, emailVerified: profile?.emailVerified,
securityKeysList: UserSecurityKeys.find({ securityKeysList: UserSecurityKeys.find({
where: { where: {
userId: user.id, userId: user.id,

View file

@ -468,6 +468,16 @@ export const packedMeDetailedOnlySchema = {
}, },
}, },
}, },
mutedPatterns: {
type: "array",
nullable: false,
optional: false,
items: {
type: "string",
nullable: false,
optional: false,
},
},
mutedInstances: { mutedInstances: {
type: "array", type: "array",
nullable: true, nullable: true,

View file

@ -60,6 +60,7 @@ export default define(meta, paramDef, async (ps, me) => {
injectFeaturedNote: profile.injectFeaturedNote, injectFeaturedNote: profile.injectFeaturedNote,
receiveAnnouncementEmail: profile.receiveAnnouncementEmail, receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
mutedWords: profile.mutedWords, mutedWords: profile.mutedWords,
mutedPatterns: profile.mutedPatterns,
mutedInstances: profile.mutedInstances, mutedInstances: profile.mutedInstances,
mutingNotificationTypes: profile.mutingNotificationTypes, mutingNotificationTypes: profile.mutingNotificationTypes,
isModerator: user.isModerator, isModerator: user.isModerator,

View file

@ -120,6 +120,7 @@ export const paramDef = {
ffVisibility: { type: "string", enum: ["public", "followers", "private"] }, ffVisibility: { type: "string", enum: ["public", "followers", "private"] },
pinnedPageId: { type: "string", format: "misskey:id", nullable: true }, pinnedPageId: { type: "string", format: "misskey:id", nullable: true },
mutedWords: { type: "array" }, mutedWords: { type: "array" },
mutedPatterns: { type: "array", items: { type: "string" } },
mutedInstances: { mutedInstances: {
type: "array", type: "array",
items: { items: {
@ -159,23 +160,52 @@ export default define(meta, paramDef, async (ps, _user, token) => {
profileUpdates.ffVisibility = ps.ffVisibility; profileUpdates.ffVisibility = ps.ffVisibility;
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) { if (ps.mutedPatterns !== undefined) {
// validate regular expression syntax for (const item of ps.mutedPatterns) {
ps.mutedWords const regexp = item.match(/^\/(.+)\/(.*)$/);
.filter((x) => !Array.isArray(x))
.forEach((x) => {
const regexp = x.match(/^\/(.+)\/(.*)$/);
if (!regexp) throw new ApiError(meta.errors.invalidRegexp); if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
try { try {
new RE2(regexp[1], regexp[2]); new RegExp(regexp[1], regexp[2]);
} catch (err) { } catch (err) {
throw new ApiError(meta.errors.invalidRegexp); throw new ApiError(meta.errors.invalidRegexp);
} }
});
profileUpdates.mutedWords = ps.mutedWords; profileUpdates.mutedPatterns = profileUpdates.mutedPatterns ?? [];
profileUpdates.enableWordMute = ps.mutedWords.length > 0; profileUpdates.mutedPatterns.push(item);
}
}
if (ps.mutedWords !== undefined) {
// for backward compatibility
for (const item of ps.mutedWords) {
if (Array.isArray(item)) continue;
const regexp = item.match(/^\/(.+)\/(.*)$/);
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
try {
new RegExp(regexp[1], regexp[2]);
} catch (err) {
throw new ApiError(meta.errors.invalidRegexp);
}
profileUpdates.mutedPatterns = profileUpdates.mutedPatterns ?? [];
profileUpdates.mutedPatterns.push(item);
}
profileUpdates.mutedWords = ps.mutedWords.filter((item) =>
Array.isArray(item),
);
}
if (
profileUpdates.mutedWords !== undefined ||
profileUpdates.mutedPatterns !== undefined
) {
profileUpdates.enableWordMute =
(profileUpdates.mutedWords != null &&
profileUpdates.mutedWords.length > 0) ||
(profileUpdates.mutedPatterns != null &&
profileUpdates.mutedPatterns.length > 0);
} }
if (ps.mutedInstances !== undefined) if (ps.mutedInstances !== undefined)
profileUpdates.mutedInstances = ps.mutedInstances; profileUpdates.mutedInstances = ps.mutedInstances;

View file

@ -69,7 +69,12 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if ( if (
this.userProfile && this.userProfile &&
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) this.user?.id !== note.userId &&
(await getWordHardMute(
note,
this.userProfile.mutedWords,
this.userProfile.mutedPatterns,
))
) )
return; return;

View file

@ -68,7 +68,12 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if ( if (
this.userProfile && this.userProfile &&
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) this.user?.id !== note.userId &&
(await getWordHardMute(
note,
this.userProfile.mutedWords,
this.userProfile.mutedPatterns,
))
) )
return; return;

View file

@ -85,7 +85,12 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if ( if (
this.userProfile && this.userProfile &&
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) this.user?.id !== note.userId &&
(await getWordHardMute(
note,
this.userProfile.mutedWords,
this.userProfile.mutedPatterns,
))
) )
return; return;

View file

@ -61,7 +61,12 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if ( if (
this.userProfile && this.userProfile &&
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) this.user?.id !== note.userId &&
(await getWordHardMute(
note,
this.userProfile.mutedWords,
this.userProfile.mutedPatterns,
))
) )
return; return;

View file

@ -83,7 +83,12 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
if ( if (
this.userProfile && this.userProfile &&
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) this.user?.id !== note.userId &&
(await getWordHardMute(
note,
this.userProfile.mutedWords,
this.userProfile.mutedPatterns,
))
) )
return; return;

View file

@ -65,9 +65,13 @@ import { inspect } from "node:util";
const logger = new Logger("create-note"); const logger = new Logger("create-note");
const mutedWordsCache = new Cache< const hardMutesCache = new Cache<
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] {
>("mutedWords", 60 * 5); userId: UserProfile["userId"];
mutedWords: UserProfile["mutedWords"];
mutedPatterns: UserProfile["mutedPatterns"];
}[]
>("hardMutes", 60 * 5);
type NotificationType = "reply" | "renote" | "quote" | "mention"; type NotificationType = "reply" | "renote" | "quote" | "mention";
@ -357,18 +361,20 @@ export default async (
incNotesCountOfUser(user); incNotesCountOfUser(user);
// Word mute // Word mute
mutedWordsCache hardMutesCache
.fetch(null, () => .fetch(null, () =>
UserProfiles.find({ UserProfiles.find({
where: { where: {
enableWordMute: true, enableWordMute: true,
}, },
select: ["userId", "mutedWords"], select: ["userId", "mutedWords", "mutedPatterns"],
}), }),
) )
.then((us) => { .then((us) => {
for (const u of us) { for (const u of us) {
getWordHardMute(data, u.userId, u.mutedWords).then((shouldMute) => { if (u.userId === user.id) return;
getWordHardMute(note, u.mutedWords, u.mutedPatterns).then(
(shouldMute: boolean) => {
if (shouldMute) { if (shouldMute) {
MutedNotes.insert({ MutedNotes.insert({
id: genId(), id: genId(),
@ -377,7 +383,8 @@ export default async (
reason: "word", reason: "word",
}); });
} }
}); },
);
} }
}); });

View file

@ -36,11 +36,15 @@
> >
<FormTextarea v-model="hardMutedWords" class="_formBlock"> <FormTextarea v-model="hardMutedWords" class="_formBlock">
<span>{{ i18n.ts._wordMute.muteWords }}</span> <span>{{ i18n.ts._wordMute.muteWords }}</span>
<template #caption <template #caption>{{
>{{ i18n.ts._wordMute.muteWordsDescription }}<br />{{ i18n.ts._wordMute.muteWordsDescription
}}</template>
</FormTextarea>
<FormTextarea v-model="hardMutedPatterns" class="_formBlock">
<span>{{ i18n.ts._wordMute.mutePatterns }}</span>
<template #caption>{{
i18n.ts._wordMute.muteWordsDescription2 i18n.ts._wordMute.muteWordsDescription2
}}</template }}</template>
>
</FormTextarea> </FormTextarea>
<MkKeyValue <MkKeyValue
v-if="hardWordMutedNotesCount != null" v-if="hardWordMutedNotesCount != null"
@ -90,6 +94,7 @@ const tab = ref("soft");
const softMutedWords = ref(render(defaultStore.state.mutedWords)); const softMutedWords = ref(render(defaultStore.state.mutedWords));
const softMutedLangs = ref(render(defaultStore.state.mutedLangs)); const softMutedLangs = ref(render(defaultStore.state.mutedLangs));
const hardMutedWords = ref(render($i!.mutedWords)); const hardMutedWords = ref(render($i!.mutedWords));
const hardMutedPatterns = ref($i!.mutedPatterns.join("\n"));
const hardWordMutedNotesCount = ref(null); const hardWordMutedNotesCount = ref(null);
const changed = ref(false); const changed = ref(false);
@ -109,8 +114,12 @@ watch(hardMutedWords, () => {
changed.value = true; changed.value = true;
}); });
watch(hardMutedPatterns, () => {
changed.value = true;
});
async function save() { async function save() {
const parseMutes = (mutes, tab) => { const parseSoftMutes = (mutes, tab) => {
// split into lines, remove empty lines and unnecessary whitespace // split into lines, remove empty lines and unnecessary whitespace
const lines = mutes const lines = mutes
.trim() .trim()
@ -151,11 +160,80 @@ async function save() {
return lines; return lines;
}; };
let softMutes, softMLangs, hardMutes; const parseMutedWords = (mutes) => {
// split into lines, remove empty lines and unnecessary whitespace
return mutes
.trim()
.split("\n")
.map((line) => line.trim())
.filter((line) => line !== "")
.map((line) => line.split(" "))
.filter((line) => line.length > 0);
};
const parseMutedPatterns = (mutes, tab) => {
// split into lines, remove empty lines and unnecessary whitespace
const lines = mutes
.trim()
.split("\n")
.map((line) => line.trim())
.filter((line) => line !== "");
// check each line if it is a RegExp or not
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const regexp = line.match(/^\/(.+)\/(.*)$/);
if (regexp) {
// check that the RegExp is valid
try { try {
softMutes = parseMutes(softMutedWords.value, i18n.ts._wordMute.soft); new RegExp(regexp[1], regexp[2]);
softMLangs = parseMutes(softMutedLangs.value, i18n.ts._wordMute.lang); // note that regex lines will not be split by spaces!
hardMutes = parseMutes(hardMutedWords.value, i18n.ts._wordMute.hard); } catch (err: any) {
// invalid syntax: do not save, do not reset changed flag
os.alert({
type: "error",
title: i18n.ts.regexpError,
text:
i18n.t("regexpErrorDescription", {
tab,
line: i + 1,
}) +
"\n" +
err.toString(),
});
// re-throw error so these invalid settings are not saved
throw err;
}
} else {
// invalid syntax: do not save, do not reset changed flag
os.alert({
type: "error",
title: i18n.ts.regexpError,
text: i18n.t("regexpErrorDescription", {
tab,
line: i + 1,
}),
});
// re-throw error so these invalid settings are not saved
throw new Error("Invalid regular expression");
}
}
return lines;
};
let softMutes, softMLangs, hardMWords, hardMPatterns;
try {
softMutes = parseSoftMutes(
softMutedWords.value,
i18n.ts._wordMute.soft,
);
softMLangs = parseMutedWords(softMutedLangs.value);
hardMWords = parseMutedWords(hardMutedWords.value);
hardMPatterns = parseMutedPatterns(
hardMutedPatterns.value,
i18n.ts._wordMute.hard,
);
} catch (err) { } catch (err) {
// already displayed error message in parseMutes // already displayed error message in parseMutes
return; return;
@ -164,7 +242,8 @@ async function save() {
defaultStore.set("mutedWords", softMutes); defaultStore.set("mutedWords", softMutes);
defaultStore.set("mutedLangs", softMLangs); defaultStore.set("mutedLangs", softMLangs);
await os.api("i/update", { await os.api("i/update", {
mutedWords: hardMutes, mutedWords: hardMWords,
mutedPatterns: hardMPatterns,
}); });
changed.value = false; changed.value = false;

View file

@ -107,6 +107,7 @@ export type MeDetailed = UserDetailed & {
isDeleted: boolean; isDeleted: boolean;
isExplorable: boolean; isExplorable: boolean;
mutedWords: string[][]; mutedWords: string[][];
mutedPatterns: string[];
mutingNotificationTypes: string[]; mutingNotificationTypes: string[];
noCrawle: boolean; noCrawle: boolean;
preventAiLearning: boolean; preventAiLearning: boolean;