From 1f37c0c5a367a19fc0b60afca943ce0a73b63a46 Mon Sep 17 00:00:00 2001 From: naskya Date: Wed, 31 Jan 2024 13:34:11 +0900 Subject: [PATCH] refactor: move misc/check-word-mute.ts to backend-rs Co-authored-by: sup39 --- locales/en-US.yml | 1 + locales/ja-JP.yml | 1 + neko/index.js | 3 +- neko/revert.sql | 5 + packages/backend-rs/Cargo.lock | 20 +- .../src/model/entity/user_profile.rs | 2 + .../backend-rs/src/util/check_word_mute.rs | 114 ++++++++ packages/backend-rs/src/util/mod.rs | 1 + ...9-separate-hard-mute-words-and-patterns.js | 32 +++ .../1644010796173-convert-hard-mutes.js | 4 +- packages/backend/package.json | 1 - packages/backend/src/misc/backend-rs.ts | 2 + .../backend/src/misc/check-hit-antenna.ts | 31 ++- packages/backend/src/misc/check-word-mute.ts | 73 ----- .../src/models/entities/user-profile.ts | 6 + .../backend/src/models/repositories/user.ts | 1 + packages/backend/src/models/schema/user.ts | 10 + .../server/api/endpoints/admin/show-user.ts | 1 + .../src/server/api/endpoints/i/update.ts | 59 +++- .../server/api/endpoints/release/translate.ts | 4 +- .../api/stream/channels/global-timeline.ts | 11 +- .../api/stream/channels/home-timeline.ts | 11 +- .../api/stream/channels/hybrid-timeline.ts | 11 +- .../api/stream/channels/local-timeline.ts | 11 +- .../stream/channels/recommended-timeline.ts | 11 +- packages/backend/src/services/note/create.ts | 40 +-- .../client/src/pages/settings/word-mute.vue | 101 ++++++- packages/firefish-js/src/entities.ts | 1 + pnpm-lock.yaml | 263 +----------------- 29 files changed, 416 insertions(+), 415 deletions(-) create mode 100644 packages/backend-rs/src/util/check_word_mute.rs create mode 100644 packages/backend/migration-neko/1706413792769-separate-hard-mute-words-and-patterns.js delete mode 100644 packages/backend/src/misc/check-word-mute.ts diff --git a/locales/en-US.yml b/locales/en-US.yml index 330baec9..a8dd71c4 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1413,6 +1413,7 @@ _menuDisplay: hide: "Hide" _wordMute: muteWords: "Muted words" + mutePatterns: "Muted patterns" muteLangs: "Muted Languages" muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 64fe2b97..971a8f10 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1221,6 +1221,7 @@ _menuDisplay: hide: "隠す" _wordMute: muteWords: "ミュートするワード" + mutePatterns: "ミュートするパターン" muteLangs: "ミュートされた言語" muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。" muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。" diff --git a/neko/index.js b/neko/index.js index 27e1e6cb..8a8c98a0 100644 --- a/neko/index.js +++ b/neko/index.js @@ -295,7 +295,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { EnvConfig, readEnvironmentConfig, readServerConfig, JsDbConn, connectToDatabase, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, stringToAcct, acctToString, getFullApAccount, isSelfHost, extractHost, toPuny, toPunyOptional, convertToHiddenPost, sqlLikeEscape, safeForSql, formatMilliseconds, nativeInitIdGenerator, nativeCreateId, nativeGetTimestamp, fetchMeta, metaToPugArgs, hasOtherRenoteOfThisNote, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, genString, IdConvertType, convertId } = nativeBinding +const { EnvConfig, readEnvironmentConfig, readServerConfig, JsDbConn, connectToDatabase, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, extractHost, toPuny, toPunyOptional, convertToHiddenPost, sqlLikeEscape, safeForSql, formatMilliseconds, nativeInitIdGenerator, nativeCreateId, nativeGetTimestamp, fetchMeta, metaToPugArgs, hasOtherRenoteOfThisNote, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, genString, IdConvertType, convertId } = nativeBinding module.exports.EnvConfig = EnvConfig module.exports.readEnvironmentConfig = readEnvironmentConfig @@ -314,6 +314,7 @@ module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum module.exports.stringToAcct = stringToAcct module.exports.acctToString = acctToString +module.exports.checkWordMute = checkWordMute module.exports.getFullApAccount = getFullApAccount module.exports.isSelfHost = isSelfHost module.exports.extractHost = extractHost diff --git a/neko/revert.sql b/neko/revert.sql index 01615574..f9ce2c7a 100644 --- a/neko/revert.sql +++ b/neko/revert.sql @@ -1,6 +1,7 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'SeparateHardMuteWordsAndPatterns1706413792769', 'RenameMetaColumns1705944717480', 'RemoveNativeUtilsMigration1705877093218', 'DropTimeZone1705691683091', @@ -12,6 +13,10 @@ DELETE FROM "migrations" WHERE name IN ( 'EmojiModerator1692825433698' ); +-- separate-hard-mute-words-and-patterns +UPDATE "user_profile" SET "mutedWords" = "mutedWords" || array_to_json("mutedPatterns")::jsonb; +ALTER TABLE "user_profile" DROP "mutedPatterns"; + -- rename-meta-columns ALTER TABLE "meta" RENAME COLUMN "tosUrl" TO "ToSUrl"; ALTER TABLE "meta" RENAME COLUMN "objectStorageUseSsl" TO "objectStorageUseSSL"; diff --git a/packages/backend-rs/Cargo.lock b/packages/backend-rs/Cargo.lock index 40993fa8..a67a04e0 100644 --- a/packages/backend-rs/Cargo.lock +++ b/packages/backend-rs/Cargo.lock @@ -94,9 +94,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220" [[package]] name = "anstyle-parse" @@ -1118,9 +1118,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -1163,9 +1163,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -2221,9 +2221,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.112" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -2244,9 +2244,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.30" +version = "0.9.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1bf28c79a99f70ee1f1d83d10c875d2e70618417fda01ad1785e027579d9d38" +checksum = "adf8a49373e98a4c5f0ceb5d05aa7c648d75f63774981ed95b7c7443bbd50c6e" dependencies = [ "indexmap", "itoa", diff --git a/packages/backend-rs/src/model/entity/user_profile.rs b/packages/backend-rs/src/model/entity/user_profile.rs index 196e5470..18b3acad 100644 --- a/packages/backend-rs/src/model/entity/user_profile.rs +++ b/packages/backend-rs/src/model/entity/user_profile.rs @@ -73,6 +73,8 @@ pub struct Model { pub prevent_ai_learning: bool, #[sea_orm(column_name = "isIndexable")] pub is_indexable: bool, + #[sea_orm(column_name = "mutedPatterns")] + pub muted_patterns: Vec, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/packages/backend-rs/src/util/check_word_mute.rs b/packages/backend-rs/src/util/check_word_mute.rs new file mode 100644 index 00000000..085383fc --- /dev/null +++ b/packages/backend-rs/src/util/check_word_mute.rs @@ -0,0 +1,114 @@ +use crate::database::JsDbConn; +use crate::model::entity::{drive_file, note}; +use once_cell::sync::Lazy; +use regex::Regex; +use sea_orm::prelude::*; + +// FIXME: remove this type +#[napi_derive::napi(object)] +pub struct NoteLike { + pub file_ids: Vec, + pub user_id: Option, + pub text: Option, + pub cw: Option, + pub renote_id: Option, + pub reply_id: Option, +} + +async fn full_text(conn: &JsDbConn, note: NoteLike) -> String { + let mut to_return = format!( + "{}\n{}\n", + note.text.unwrap_or_default(), + note.cw.unwrap_or_default(), + ); + + for file_id in note.file_ids { + if let Some(alt_text) = drive_file::Entity::find_by_id(file_id) + .one(conn.inner()) + .await + .expect("Failed to connect to the database") + .expect("file_id is invalid") + .comment + { + to_return.push_str(alt_text.as_str()); + to_return.push('\n'); + } + } + + if note.renote_id.is_some() { + to_return.push_str( + note::Entity::find_by_id(note.renote_id.unwrap()) + .one(conn.inner()) + .await + .expect("Failed to connect to the database") + .expect("renote_id is invalid") + .text + .unwrap_or_default() + .as_str(), + ); + to_return.push('\n'); + } + + if note.reply_id.is_some() { + to_return.push_str( + note::Entity::find_by_id(note.reply_id.unwrap()) + .one(conn.inner()) + .await + .expect("Failed to connect to the database") + .expect("reply_id is invalid") + .text + .unwrap_or_default() + .as_str(), + ); + } + + to_return.trim().to_owned() +} + +// FIXME: remove this funtion +fn convert_regex(js_regex: String) -> String { + static RE: Lazy = Lazy::new(|| Regex::new(r"^/(.+)/(.*)$").unwrap()); + RE.replace(&js_regex, "(?$2)$1").to_string() +} + +async fn check_word_mute_impl( + text: String, + muted_word_lists: Vec>, + muted_patterns: Vec, +) -> bool { + if text.is_empty() { + return false; + } + + let text_lowercase = text.to_lowercase(); + + muted_word_lists.into_iter().any(|muted_word_list| { + muted_word_list + .into_iter() + .all(|muted_word| text_lowercase.contains(&muted_word.to_lowercase())) + }) || muted_patterns.into_iter().any(|muted_patten| { + match Regex::new(convert_regex(muted_patten).as_str()) { + Ok(re) => re.is_match(&text), + Err(_) => false, + } + }) +} + +#[napi_derive::napi] +pub async fn check_word_mute( + conn: &JsDbConn, + note: NoteLike, + muted_word_lists: Vec>, + muted_patterns: Vec, +) -> bool { + if muted_word_lists.is_empty() && muted_patterns.is_empty() { + false + } else { + check_word_mute_impl( + full_text(conn, note).await, + muted_word_lists, + muted_patterns, + ) + .await + } +} diff --git a/packages/backend-rs/src/util/mod.rs b/packages/backend-rs/src/util/mod.rs index 01102afa..0c678940 100644 --- a/packages/backend-rs/src/util/mod.rs +++ b/packages/backend-rs/src/util/mod.rs @@ -1,4 +1,5 @@ pub mod acct; +pub mod check_word_mute; pub mod convert_host; pub mod convert_to_hidden_post; pub mod escape_sql; diff --git a/packages/backend/migration-neko/1706413792769-separate-hard-mute-words-and-patterns.js b/packages/backend/migration-neko/1706413792769-separate-hard-mute-words-and-patterns.js new file mode 100644 index 00000000..ace57340 --- /dev/null +++ b/packages/backend/migration-neko/1706413792769-separate-hard-mute-words-and-patterns.js @@ -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"`); + } +} diff --git a/packages/backend/migration/1644010796173-convert-hard-mutes.js b/packages/backend/migration/1644010796173-convert-hard-mutes.js index 39c5b080..df1e02cb 100644 --- a/packages/backend/migration/1644010796173-convert-hard-mutes.js +++ b/packages/backend/migration/1644010796173-convert-hard-mutes.js @@ -1,5 +1,3 @@ -import RE2 from "re2"; - export class convertHardMutes1644010796173 { name = "convertHardMutes1644010796173"; @@ -15,7 +13,7 @@ export class convertHardMutes1644010796173 { if (regexp) { // convert regexp's try { - new RE2(regexp[1], regexp[2]); + new RegExp(regexp[1], regexp[2]); return `/${regexp[1]}/${regexp[2]}`; } catch (err) { // invalid regex, ignore it diff --git a/packages/backend/package.json b/packages/backend/package.json index 4261ac8a..4557d77d 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -98,7 +98,6 @@ "qs": "6.11.2", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.20.9", "redis-semaphore": "5.5.0", "reflect-metadata": "0.2.1", "rename": "1.0.4", diff --git a/packages/backend/src/misc/backend-rs.ts b/packages/backend/src/misc/backend-rs.ts index 97521a10..3bddd80e 100644 --- a/packages/backend/src/misc/backend-rs.ts +++ b/packages/backend/src/misc/backend-rs.ts @@ -2,6 +2,7 @@ import { readServerConfig, readEnvironmentConfig, connectToDatabase, + checkWordMute as checkWordMuteImpl, fetchMeta as fetchMetaImpl, hasOtherRenoteOfThisNote as hasOtherRenoteOfThisNoteImpl, JsDbConn, @@ -20,3 +21,4 @@ const curryDb = export const fetchMeta = curryDb(fetchMetaImpl); export const hasOtherRenoteOfThisNote = curryDb(hasOtherRenoteOfThisNoteImpl); +export const checkWordMute = curryDb(checkWordMuteImpl); diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts index 7a2d6b4f..fdeedaf3 100644 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ b/packages/backend/src/misc/check-hit-antenna.ts @@ -1,14 +1,19 @@ import type { Antenna } from "@/models/entities/antenna.js"; import type { Note } from "@/models/entities/note.js"; import type { User } from "@/models/entities/user.js"; +import type { UserProfile } from "@/models/entities/user-profile.js"; import { Blockings, Followings, UserProfiles } from "@/models/index.js"; import { getFullApAccount, stringToAcct } from "backend-rs"; +import { checkWordMute } from "@/misc/backend-rs.js"; import type { Packed } from "@/misc/schema.js"; import { Cache } from "@/misc/cache.js"; -import { getWordHardMute } from "@/misc/check-word-mute.js"; const blockingCache = new Cache("blocking", 60 * 5); -const mutedWordsCache = new Cache("mutedWords", 60 * 5); +const hardMutesCache = new Cache<{ + userId: UserProfile["userId"]; + mutedWords: UserProfile["mutedWords"]; + mutedPatterns: UserProfile["mutedPatterns"]; +}>("hardMutes", 60 * 5); const followingCache = new Cache("following", 60 * 5); export async function checkHitAntenna( @@ -103,12 +108,24 @@ export async function checkHitAntenna( if (!following.includes(note.userId)) return false; } - const mutedWords = await mutedWordsCache.fetch(antenna.userId, () => - UserProfiles.findOneBy({ userId: antenna.userId }).then( - (profile) => profile?.mutedWords, - ), + const mutes = await hardMutesCache.fetch(antenna.userId, () => + UserProfiles.findOneByOrFail({ + 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 checkWordMute(note, mutes.mutedWords, mutes.mutedPatterns)) + ) + return false; // TODO: eval expression diff --git a/packages/backend/src/misc/check-word-mute.ts b/packages/backend/src/misc/check-word-mute.ts deleted file mode 100644 index 5686aef2..00000000 --- a/packages/backend/src/misc/check-word-mute.ts +++ /dev/null @@ -1,73 +0,0 @@ -import RE2 from "re2"; -import type { Note } from "@/models/entities/note.js"; - -type NoteLike = { - userId: Note["userId"]; - text: Note["text"]; - files?: Note["files"]; - cw?: Note["cw"]; -}; - -function checkWordMute( - note: NoteLike, - mutedWords: Array, -): boolean { - if (note == null) return false; - - let text = `${note.cw ?? ""} ${note.text ?? ""}`; - if (note.files != null) - text += ` ${note.files.map((f) => f.comment ?? "").join(" ")}`; - text = text.trim(); - - if (text === "") return false; - - for (const mutePattern of mutedWords) { - if (Array.isArray(mutePattern)) { - // Clean up - const keywords = mutePattern.filter((keyword) => keyword !== ""); - - if ( - keywords.length > 0 && - keywords.every((keyword) => - text.toLowerCase().includes(keyword.toLowerCase()), - ) - ) - return true; - } else { - // represents RegExp - const regexp = mutePattern.match(/^\/(.+)\/(.*)$/); - - // This should never happen due to input sanitisation. - if (!regexp) { - console.warn(`Found invalid regex in word mutes: ${mutePattern}`); - continue; - } - - try { - if (new RE2(regexp[1], regexp[2]).test(text)) return true; - } catch (err) { - // This should never happen due to input sanitisation. - } - } - } - - return false; -} - -export async function getWordHardMute( - note: NoteLike, - meId: string | null | undefined, - mutedWords?: Array, -): Promise { - if (note.userId === meId || mutedWords == null) return false; - - if (mutedWords.length > 0) { - return ( - checkWordMute(note, mutedWords) || - checkWordMute(note.reply, mutedWords) || - checkWordMute(note.renote, mutedWords) - ); - } - - return false; -} diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts index fcf3b37f..78a6fcc0 100644 --- a/packages/backend/src/models/entities/user-profile.ts +++ b/packages/backend/src/models/entities/user-profile.ts @@ -223,6 +223,12 @@ export class UserProfile { }) public mutedWords: string[][]; + @Column("text", { + array: true, + nullable: false, + }) + public mutedPatterns: string[]; + @Column("jsonb", { default: [], comment: "List of instances muted by the user.", diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 2104394f..b3f498fc 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -573,6 +573,7 @@ export const UserRepository = db.getRepository(User).extend({ hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id), mutedWords: profile?.mutedWords, + mutedPatterns: profile?.mutedPatterns, mutedInstances: profile?.mutedInstances, mutingNotificationTypes: profile?.mutingNotificationTypes, emailNotificationTypes: profile?.emailNotificationTypes, diff --git a/packages/backend/src/models/schema/user.ts b/packages/backend/src/models/schema/user.ts index 8e3d612a..c7417ce0 100644 --- a/packages/backend/src/models/schema/user.ts +++ b/packages/backend/src/models/schema/user.ts @@ -474,6 +474,16 @@ export const packedMeDetailedOnlySchema = { }, }, }, + mutedPatterns: { + type: "array", + nullable: false, + optional: false, + items: { + type: "string", + nullable: false, + optional: false, + }, + }, mutedInstances: { type: "array", nullable: true, diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 2f6d5801..2a7f6538 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -61,6 +61,7 @@ export default define(meta, paramDef, async (ps, me) => { injectFeaturedNote: profile.injectFeaturedNote, receiveAnnouncementEmail: profile.receiveAnnouncementEmail, mutedWords: profile.mutedWords, + mutedPatterns: profile.mutedPatterns, mutedInstances: profile.mutedInstances, mutingNotificationTypes: profile.mutingNotificationTypes, isModerator: user.isModerator, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index a0bf2e8c..6d5ea4c3 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -1,4 +1,3 @@ -import RE2 from "re2"; import * as mfm from "mfm-js"; import { publishMainStream, publishUserEvent } from "@/services/stream.js"; import acceptAllFollowRequests from "@/services/following/requests/accept-all.js"; @@ -126,6 +125,7 @@ export const paramDef = { ffVisibility: { type: "string", enum: ["public", "followers", "private"] }, pinnedPageId: { type: "string", format: "misskey:id", nullable: true }, mutedWords: { type: "array" }, + mutedPatterns: { type: "array", items: { type: "string" } }, mutedInstances: { type: "array", items: { @@ -166,23 +166,52 @@ export default define(meta, paramDef, async (ps, _user, token) => { profileUpdates.ffVisibility = ps.ffVisibility; if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; + if (ps.mutedPatterns !== undefined) { + for (const item of ps.mutedPatterns) { + 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); + } + } if (ps.mutedWords !== undefined) { - // validate regular expression syntax - ps.mutedWords - .filter((x) => !Array.isArray(x)) - .forEach((x) => { - const regexp = x.match(/^\/(.+)\/(.*)$/); - if (!regexp) throw new ApiError(meta.errors.invalidRegexp); + // for compatibility with the upstream Firefish API + for (const item of ps.mutedWords) { + if (Array.isArray(item)) continue; - try { - new RE2(regexp[1], regexp[2]); - } catch (err) { - throw new ApiError(meta.errors.invalidRegexp); - } - }); + const regexp = item.match(/^\/(.+)\/(.*)$/); + if (!regexp) throw new ApiError(meta.errors.invalidRegexp); - profileUpdates.mutedWords = ps.mutedWords; - profileUpdates.enableWordMute = ps.mutedWords.length > 0; + 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) profileUpdates.mutedInstances = ps.mutedInstances; diff --git a/packages/backend/src/server/api/endpoints/release/translate.ts b/packages/backend/src/server/api/endpoints/release/translate.ts index fa3d3df7..0a87ce05 100644 --- a/packages/backend/src/server/api/endpoints/release/translate.ts +++ b/packages/backend/src/server/api/endpoints/release/translate.ts @@ -4,7 +4,6 @@ import { fileURLToPath } from "node:url"; import define from "@/server/api/define.js"; import translate from "@/misc/translate.js"; import type { Language } from "@/misc/langmap.js"; -import RE2 from "re2"; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -37,8 +36,7 @@ export const paramDef = { } as const; async function translateCommitMsg(msg: string, targetLang: Language) { - const regex = new RE2(/^(.*) (\(by .*\))$/); - const matches = regex.match(msg); + const matches = msg.match(/^(.*) (\(by .*\))$/); if (matches == null) return msg; diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 3042f642..c887cb98 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -1,9 +1,9 @@ import Channel from "../channel.js"; import { fetchMeta } from "@/misc/backend-rs.js"; -import { getWordHardMute } from "@/misc/check-word-mute.js"; import { isInstanceMuted } from "@/misc/is-instance-muted.js"; import { isUserRelated } from "@/misc/is-user-related.js"; import type { Packed } from "@/misc/schema.js"; +import { checkWordMute } from "@/misc/backend-rs.js"; export default class extends Channel { public readonly chName = "globalTimeline"; @@ -68,10 +68,15 @@ export default class extends Channel { // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる + // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる if ( this.userProfile && - (await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) + this.user?.id !== note.userId && + (await checkWordMute( + note, + this.userProfile.mutedWords, + this.userProfile.mutedPatterns, + )) ) return; diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 4f2d1e0e..10d860f4 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -1,8 +1,8 @@ import Channel from "../channel.js"; -import { getWordHardMute } from "@/misc/check-word-mute.js"; import { isUserRelated } from "@/misc/is-user-related.js"; import { isInstanceMuted } from "@/misc/is-instance-muted.js"; import type { Packed } from "@/misc/schema.js"; +import { checkWordMute } from "@/misc/backend-rs.js"; export default class extends Channel { public readonly chName = "homeTimeline"; @@ -65,10 +65,15 @@ export default class extends Channel { // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる + // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる if ( this.userProfile && - (await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) + this.user?.id !== note.userId && + (await checkWordMute( + note, + this.userProfile.mutedWords, + this.userProfile.mutedPatterns, + )) ) return; diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 83c01846..eb1af79f 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -1,9 +1,9 @@ import Channel from "../channel.js"; import { fetchMeta } from "@/misc/backend-rs.js"; -import { getWordHardMute } from "@/misc/check-word-mute.js"; import { isUserRelated } from "@/misc/is-user-related.js"; import { isInstanceMuted } from "@/misc/is-instance-muted.js"; import type { Packed } from "@/misc/schema.js"; +import { checkWordMute } from "@/misc/backend-rs.js"; export default class extends Channel { public readonly chName = "hybridTimeline"; @@ -82,10 +82,15 @@ export default class extends Channel { // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる + // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる if ( this.userProfile && - (await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) + this.user?.id !== note.userId && + (await checkWordMute( + note, + this.userProfile.mutedWords, + this.userProfile.mutedPatterns, + )) ) return; diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index e3e484a9..51205e9d 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -1,8 +1,8 @@ import Channel from "../channel.js"; import { fetchMeta } from "@/misc/backend-rs.js"; -import { getWordHardMute } from "@/misc/check-word-mute.js"; import { isUserRelated } from "@/misc/is-user-related.js"; import type { Packed } from "@/misc/schema.js"; +import { checkWordMute } from "@/misc/backend-rs.js"; export default class extends Channel { public readonly chName = "localTimeline"; @@ -60,10 +60,15 @@ export default class extends Channel { // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる + // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる if ( this.userProfile && - (await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) + this.user?.id !== note.userId && + (await checkWordMute( + note, + this.userProfile.mutedWords, + this.userProfile.mutedPatterns, + )) ) return; diff --git a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts index a74c44a8..580e5dfc 100644 --- a/packages/backend/src/server/api/stream/channels/recommended-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/recommended-timeline.ts @@ -1,9 +1,9 @@ import Channel from "../channel.js"; import { fetchMeta } from "@/misc/backend-rs.js"; -import { getWordHardMute } from "@/misc/check-word-mute.js"; import { isUserRelated } from "@/misc/is-user-related.js"; import { isInstanceMuted } from "@/misc/is-instance-muted.js"; import type { Packed } from "@/misc/schema.js"; +import { checkWordMute } from "@/misc/backend-rs.js"; export default class extends Channel { public readonly chName = "recommendedTimeline"; @@ -80,10 +80,15 @@ export default class extends Channel { // TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) // 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 // レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 - // そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる + // そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる if ( this.userProfile && - (await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords)) + this.user?.id !== note.userId && + (await checkWordMute( + note, + this.userProfile.mutedWords, + this.userProfile.mutedPatterns, + )) ) return; diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 32f8f59f..814b869e 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -45,9 +45,8 @@ import { Poll } from "@/models/entities/poll.js"; import { createNotification } from "@/services/create-notification.js"; import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; import { checkHitAntenna } from "@/misc/check-hit-antenna.js"; -import { getWordHardMute } from "@/misc/check-word-mute.js"; import { addNoteToAntenna } from "@/services/add-note-to-antenna.js"; -import { hasOtherRenoteOfThisNote } from "@/misc/backend-rs.js"; +import { checkWordMute, hasOtherRenoteOfThisNote } from "@/misc/backend-rs.js"; import { deliverToRelays, getCachedRelays } from "../relay.js"; import type { Channel } from "@/models/entities/channel.js"; import { normalizeForSearch } from "@/misc/normalize-for-search.js"; @@ -64,9 +63,13 @@ import { redisClient } from "@/db/redis.js"; import { Mutex } from "redis-semaphore"; import { langmap } from "@/misc/langmap.js"; -const mutedWordsCache = new Cache< - { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] ->("mutedWords", 60 * 5); +const hardMutesCache = new Cache< + { + userId: UserProfile["userId"]; + mutedWords: UserProfile["mutedWords"]; + mutedPatterns: UserProfile["mutedPatterns"]; + }[] +>("hardMutes", 60 * 5); type NotificationType = "reply" | "renote" | "quote" | "mention"; @@ -357,27 +360,30 @@ export default async ( incNotesCountOfUser(user); // Word mute - mutedWordsCache + hardMutesCache .fetch(null, () => UserProfiles.find({ where: { enableWordMute: true, }, - select: ["userId", "mutedWords"], + select: ["userId", "mutedWords", "mutedPatterns"], }), ) .then((us) => { for (const u of us) { - getWordHardMute(data, u.userId, u.mutedWords).then((shouldMute) => { - if (shouldMute) { - MutedNotes.insert({ - id: genId(), - userId: u.userId, - noteId: note.id, - reason: "word", - }); - } - }); + if (u.userId === user.id) return; + checkWordMute(note, u.mutedWords, u.mutedPatterns).then( + (shouldMute: boolean) => { + if (shouldMute) { + MutedNotes.insert({ + id: genId(), + userId: u.userId, + noteId: note.id, + reason: "word", + }); + } + }, + ); } }); diff --git a/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue index 68b6b09a..8a6a0408 100644 --- a/packages/client/src/pages/settings/word-mute.vue +++ b/packages/client/src/pages/settings/word-mute.vue @@ -36,11 +36,15 @@ > {{ i18n.ts._wordMute.muteWords }} - + + + + {{ i18n.ts._wordMute.mutePatterns }} + { changed.value = true; }); +watch(hardMutedPatterns, () => { + changed.value = true; +}); + async function save() { - const parseMutes = (mutes, tab) => { + const parseSoftMutes = (mutes, tab) => { // split into lines, remove empty lines and unnecessary whitespace const lines = mutes .trim() @@ -151,11 +160,80 @@ async function save() { 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 { + new RegExp(regexp[1], regexp[2]); + // note that regex lines will not be split by spaces! + } 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 = parseMutes(softMutedWords.value, i18n.ts._wordMute.soft); - softMLangs = parseMutes(softMutedLangs.value, i18n.ts._wordMute.lang); - hardMutes = parseMutes(hardMutedWords.value, i18n.ts._wordMute.hard); + 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) { // already displayed error message in parseMutes return; @@ -164,7 +242,8 @@ async function save() { defaultStore.set("mutedWords", softMutes); defaultStore.set("mutedLangs", softMLangs); await os.api("i/update", { - mutedWords: hardMutes, + mutedWords: hardMWords, + mutedPatterns: hardMPatterns, }); changed.value = false; diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts index 6d434089..ed028efa 100644 --- a/packages/firefish-js/src/entities.ts +++ b/packages/firefish-js/src/entities.ts @@ -107,6 +107,7 @@ export type MeDetailed = UserDetailed & { isDeleted: boolean; isExplorable: boolean; mutedWords: string[][]; + mutedPatterns: string[]; mutingNotificationTypes: string[]; noCrawle: boolean; preventAiLearning: boolean; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4b6a839..6ce537d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -300,9 +300,6 @@ importers: ratelimiter: specifier: 3.4.1 version: 3.4.1 - re2: - specifier: 1.20.9 - version: 1.20.9 redis-semaphore: specifier: 5.5.0 version: 5.5.0(ioredis@5.3.2) @@ -3017,26 +3014,6 @@ packages: fastq: 1.15.0 dev: true - /@npmcli/agent@2.2.0: - resolution: {integrity: sha512-2yThA1Es98orMkpSLVqlDZAMPK3jHJhifP2gnNUdk1754uZ8yI5c+ulCoVG+WlntQA6MzhrURMXjSd9Z7dJ2/Q==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - agent-base: 7.1.0 - http-proxy-agent: 7.0.0 - https-proxy-agent: 7.0.2 - lru-cache: 10.0.2 - socks-proxy-agent: 8.0.2 - transitivePeerDependencies: - - supports-color - dev: false - - /@npmcli/fs@3.1.0: - resolution: {integrity: sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - semver: 7.5.4 - dev: false - /@one-ini/wasm@0.1.1: resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} dev: false @@ -4757,14 +4734,6 @@ packages: - supports-color dev: false - /aggregate-error@3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - dev: false - /ajv-keywords@3.5.2(ajv@6.12.6): resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -5708,24 +5677,6 @@ packages: engines: {node: '>= 0.8'} dev: false - /cacache@18.0.0: - resolution: {integrity: sha512-I7mVOPl3PUCeRub1U8YoGz2Lqv9WOBpobZ8RyWFXmReuILz+3OAyTa5oH3QPdtKZD7N0Yk00aLfzn0qvp8dZ1w==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - '@npmcli/fs': 3.1.0 - fs-minipass: 3.0.3 - glob: 10.3.10 - lru-cache: 10.0.2 - minipass: 7.0.4 - minipass-collect: 1.0.2 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - p-map: 4.0.0 - ssri: 10.0.5 - tar: 6.2.0 - unique-filename: 3.0.0 - dev: false - /cache-base@1.0.1: resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} engines: {node: '>=0.10.0'} @@ -6092,11 +6043,6 @@ packages: escape-string-regexp: 1.0.5 dev: true - /clean-stack@2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - dev: false - /cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -7338,14 +7284,6 @@ packages: engines: {node: '>= 0.8'} dev: false - /encoding@0.1.13: - resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} - requiresBuild: true - dependencies: - iconv-lite: 0.6.3 - dev: false - optional: true - /end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: @@ -7371,15 +7309,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - /env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - dev: false - - /err-code@2.0.3: - resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - dev: false - /error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} dependencies: @@ -8560,10 +8489,6 @@ packages: jest-util: 29.7.0 dev: true - /exponential-backoff@3.1.1: - resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} - dev: false - /ext-list@2.2.2: resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} engines: {node: '>=0.10.0'} @@ -9059,13 +8984,6 @@ packages: dependencies: minipass: 3.3.6 - /fs-minipass@3.0.3: - resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - minipass: 7.0.4 - dev: false - /fs-mkdirp-stream@1.0.0: resolution: {integrity: sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==} engines: {node: '>= 0.10'} @@ -9769,16 +9687,6 @@ packages: toidentifier: 1.0.1 dev: false - /http-proxy-agent@7.0.0: - resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.0 - debug: 4.3.4 - transitivePeerDependencies: - - supports-color - dev: false - /http2-wrapper@1.0.3: resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} engines: {node: '>=10.19.0'} @@ -9890,10 +9798,12 @@ packages: /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + dev: true /indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + dev: true /indent-string@5.0.0: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} @@ -9930,11 +9840,6 @@ packages: resolution: {integrity: sha512-/nPtyeX9xPUvxZf+r0518B7uqNKlP+LqNJqSiXFEaa2T71rWIwTVXGH7hB9xO/EVdwa5/pWlFCPwShOW81XIxQ==} dev: true - /install-artifact-from-github@1.3.5: - resolution: {integrity: sha512-gZHC7f/cJgXz7MXlHFBxPVMsvIbev1OQN1uKQYKVJDydGNm9oYf9JstbU4Atnh/eSvk41WtEovoRm+8IF686xg==} - hasBin: true - dev: false - /internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} engines: {node: '>= 0.4'} @@ -10238,10 +10143,6 @@ packages: ip-regex: 4.3.0 dev: false - /is-lambda@1.0.1: - resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} - dev: false - /is-natural-number@4.0.1: resolution: {integrity: sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==} dev: false @@ -10436,11 +10337,6 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - /isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} - dev: false - /isobject@2.1.0: resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} engines: {node: '>=0.10.0'} @@ -11815,25 +11711,6 @@ packages: /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - /make-fetch-happen@13.0.0: - resolution: {integrity: sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==} - engines: {node: ^16.14.0 || >=18.0.0} - dependencies: - '@npmcli/agent': 2.2.0 - cacache: 18.0.0 - http-cache-semantics: 4.1.1 - is-lambda: 1.0.1 - minipass: 7.0.4 - minipass-fetch: 3.0.4 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - negotiator: 0.6.3 - promise-retry: 2.0.1 - ssri: 10.0.5 - transitivePeerDependencies: - - supports-color - dev: false - /make-iterator@1.0.1: resolution: {integrity: sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==} engines: {node: '>=0.10.0'} @@ -12103,45 +11980,6 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - /minipass-collect@1.0.2: - resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - dev: false - - /minipass-fetch@3.0.4: - resolution: {integrity: sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - minipass: 7.0.4 - minipass-sized: 1.0.3 - minizlib: 2.1.2 - optionalDependencies: - encoding: 0.1.13 - dev: false - - /minipass-flush@1.0.5: - resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - dev: false - - /minipass-pipeline@1.2.4: - resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} - engines: {node: '>=8'} - dependencies: - minipass: 3.3.6 - dev: false - - /minipass-sized@1.0.3: - resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} - engines: {node: '>=8'} - dependencies: - minipass: 3.3.6 - dev: false - /minipass@3.3.6: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} @@ -12264,7 +12102,9 @@ packages: /nan@2.18.0: resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==} + requiresBuild: true dev: false + optional: true /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} @@ -12377,25 +12217,6 @@ packages: resolution: {integrity: sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==} hasBin: true - /node-gyp@10.0.1: - resolution: {integrity: sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==} - engines: {node: ^16.14.0 || >=18.0.0} - hasBin: true - dependencies: - env-paths: 2.2.1 - exponential-backoff: 3.1.1 - glob: 10.3.10 - graceful-fs: 4.2.11 - make-fetch-happen: 13.0.0 - nopt: 7.2.0 - proc-log: 3.0.0 - semver: 7.5.4 - tar: 6.2.0 - which: 4.0.0 - transitivePeerDependencies: - - supports-color - dev: false - /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true @@ -12795,13 +12616,6 @@ packages: p-limit: 3.1.0 dev: true - /p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - dependencies: - aggregate-error: 3.1.0 - dev: false - /p-map@6.0.0: resolution: {integrity: sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw==} engines: {node: '>=16'} @@ -13655,11 +13469,6 @@ packages: - supports-color dev: false - /proc-log@3.0.0: - resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: false - /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -13667,14 +13476,6 @@ packages: resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} dev: false - /promise-retry@2.0.1: - resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} - engines: {node: '>=10'} - dependencies: - err-code: 2.0.3 - retry: 0.12.0 - dev: false - /promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} dependencies: @@ -13922,17 +13723,6 @@ packages: setimmediate: 1.0.5 dev: false - /re2@1.20.9: - resolution: {integrity: sha512-ZYcPTFr5ha2xq3WQjBDTF9CWPSDK1z28MLh5UFRxc//7X8BNQ3A7yR7ITnP0jO346661ertdKVFqw1qoL3FMEQ==} - requiresBuild: true - dependencies: - install-artifact-from-github: 1.3.5 - nan: 2.18.0 - node-gyp: 10.0.1 - transitivePeerDependencies: - - supports-color - dev: false - /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true @@ -14316,11 +14106,6 @@ packages: engines: {node: '>=0.12'} dev: false - /retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - dev: false - /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -14730,17 +14515,6 @@ packages: - supports-color dev: false - /socks-proxy-agent@8.0.2: - resolution: {integrity: sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.0 - debug: 4.3.4 - socks: 2.7.1 - transitivePeerDependencies: - - supports-color - dev: false - /socks@2.7.1: resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} @@ -14882,13 +14656,6 @@ packages: tweetnacl: 0.14.5 dev: false - /ssri@10.0.5: - resolution: {integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - minipass: 7.0.4 - dev: false - /stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} dev: false @@ -16042,20 +15809,6 @@ packages: resolution: {integrity: sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ==} dev: false - /unique-filename@3.0.0: - resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - unique-slug: 4.0.0 - dev: false - - /unique-slug@4.0.0: - resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - imurmurhash: 0.1.4 - dev: false - /unique-stream@2.3.1: resolution: {integrity: sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==} dependencies: @@ -16578,14 +16331,6 @@ packages: dependencies: isexe: 2.0.0 - /which@4.0.0: - resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} - engines: {node: ^16.13.0 || >=18.0.0} - hasBin: true - dependencies: - isexe: 3.1.1 - dev: false - /wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} requiresBuild: true