refactor: move misc/check-word-mute.ts to backend-rs

Co-authored-by: sup39 <dev@sup39.dev>
This commit is contained in:
naskya 2024-01-31 13:34:11 +09:00
parent 45a65d156e
commit 1f37c0c5a3
Signed by: naskya
GPG key ID: 712D413B3A9FED5C
29 changed files with 416 additions and 415 deletions

View file

@ -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."

View file

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

View file

@ -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

View file

@ -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";

View file

@ -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",

View file

@ -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<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -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<String>,
pub user_id: Option<String>,
pub text: Option<String>,
pub cw: Option<String>,
pub renote_id: Option<String>,
pub reply_id: Option<String>,
}
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<Regex> = 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<Vec<String>>,
muted_patterns: Vec<String>,
) -> 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<Vec<String>>,
muted_patterns: Vec<String>,
) -> 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
}
}

View file

@ -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;

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,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

View file

@ -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",

View file

@ -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);

View file

@ -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<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);
const followingCache = new Cache<User["id"][]>("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

View file

@ -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<string | string[]>,
): 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<string | string[]>,
): Promise<boolean> {
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;
}

View file

@ -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.",

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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",
});
}
},
);
}
});

View file

@ -36,11 +36,15 @@
>
<FormTextarea v-model="hardMutedWords" class="_formBlock">
<span>{{ i18n.ts._wordMute.muteWords }}</span>
<template #caption
>{{ i18n.ts._wordMute.muteWordsDescription }}<br />{{
i18n.ts._wordMute.muteWordsDescription2
}}</template
>
<template #caption>{{
i18n.ts._wordMute.muteWordsDescription
}}</template>
</FormTextarea>
<FormTextarea v-model="hardMutedPatterns" class="_formBlock">
<span>{{ i18n.ts._wordMute.mutePatterns }}</span>
<template #caption>{{
i18n.ts._wordMute.muteWordsDescription2
}}</template>
</FormTextarea>
<MkKeyValue
v-if="hardWordMutedNotesCount != null"
@ -90,6 +94,7 @@ const tab = ref("soft");
const softMutedWords = ref(render(defaultStore.state.mutedWords));
const softMutedLangs = ref(render(defaultStore.state.mutedLangs));
const hardMutedWords = ref(render($i!.mutedWords));
const hardMutedPatterns = ref($i!.mutedPatterns.join("\n"));
const hardWordMutedNotesCount = ref(null);
const changed = ref(false);
@ -109,8 +114,12 @@ watch(hardMutedWords, () => {
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;

View file

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

View file

@ -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