refactor (backend): separate muted words and muted patterns
Co-authored-by: sup39 <dev@sup39.dev>
This commit is contained in:
parent
b30e68c98c
commit
d70d0f4232
21 changed files with 319 additions and 97 deletions
|
@ -5,6 +5,7 @@ Critical security updates are indicated by the :warning: icon.
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- Introduce new full-text search engine and post search filters
|
- Introduce new full-text search engine and post search filters
|
||||||
|
- Refactoring
|
||||||
|
|
||||||
## v20240301
|
## v20240301
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
DELETE FROM "migrations" WHERE name IN (
|
DELETE FROM "migrations" WHERE name IN (
|
||||||
|
'SeparateHardMuteWordsAndPatterns1706413792769',
|
||||||
'IndexAltTextAndCw1708872574733',
|
'IndexAltTextAndCw1708872574733',
|
||||||
'Pgroonga1698420787202',
|
'Pgroonga1698420787202',
|
||||||
'ChangeDefaultConfigs1709251460718',
|
'ChangeDefaultConfigs1709251460718',
|
||||||
|
@ -14,6 +15,10 @@ DELETE FROM "migrations" WHERE name IN (
|
||||||
'RemoveNativeUtilsMigration1705877093218'
|
'RemoveNativeUtilsMigration1705877093218'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- separate-hard-mute-words-and-patterns
|
||||||
|
UPDATE "user_profile" SET "mutedWords" = "mutedWords" || array_to_json("mutedPatterns")::jsonb;
|
||||||
|
ALTER TABLE "user_profile" DROP "mutedPatterns";
|
||||||
|
|
||||||
-- index-alt-text-and-cw
|
-- index-alt-text-and-cw
|
||||||
DROP INDEX "IDX_f4f7b93d05958527300d79ac82";
|
DROP INDEX "IDX_f4f7b93d05958527300d79ac82";
|
||||||
DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f";
|
DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f";
|
||||||
|
|
|
@ -1429,6 +1429,7 @@ _menuDisplay:
|
||||||
hide: "Hide"
|
hide: "Hide"
|
||||||
_wordMute:
|
_wordMute:
|
||||||
muteWords: "Muted words"
|
muteWords: "Muted words"
|
||||||
|
mutePatterns: "Muted patterns"
|
||||||
muteLangs: "Muted Languages"
|
muteLangs: "Muted Languages"
|
||||||
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks
|
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks
|
||||||
for an OR condition."
|
for an OR condition."
|
||||||
|
|
|
@ -1220,7 +1220,8 @@ _menuDisplay:
|
||||||
hide: "隠す"
|
hide: "隠す"
|
||||||
_wordMute:
|
_wordMute:
|
||||||
muteWords: "ミュートするワード"
|
muteWords: "ミュートするワード"
|
||||||
muteLangs: "ミュートされた言語"
|
mutePatterns: "ミュートするパターン"
|
||||||
|
muteLangs: "ミュートする言語"
|
||||||
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
|
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
|
||||||
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
|
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
|
||||||
muteLangsDescription: "OR 条件の場合はスペースまたは改行で区切ります。"
|
muteLangsDescription: "OR 条件の場合はスペースまたは改行で区切ります。"
|
||||||
|
|
|
@ -71,6 +71,8 @@ pub struct Model {
|
||||||
pub prevent_ai_learning: bool,
|
pub prevent_ai_learning: bool,
|
||||||
#[sea_orm(column_name = "isIndexable")]
|
#[sea_orm(column_name = "isIndexable")]
|
||||||
pub is_indexable: bool,
|
pub is_indexable: bool,
|
||||||
|
#[sea_orm(column_name = "mutedPatterns")]
|
||||||
|
pub muted_patterns: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Antenna } from "@/models/entities/antenna.js";
|
import type { Antenna } from "@/models/entities/antenna.js";
|
||||||
import type { Note } from "@/models/entities/note.js";
|
import type { Note } from "@/models/entities/note.js";
|
||||||
import type { User } from "@/models/entities/user.js";
|
import type { User } from "@/models/entities/user.js";
|
||||||
|
import type { UserProfile } from "@/models/entities/user-profile.js";
|
||||||
import { Blockings, UserProfiles } from "@/models/index.js";
|
import { Blockings, UserProfiles } from "@/models/index.js";
|
||||||
import { getFullApAccount } from "@/misc/convert-host.js";
|
import { getFullApAccount } from "@/misc/convert-host.js";
|
||||||
import * as Acct from "@/misc/acct.js";
|
import * as Acct from "@/misc/acct.js";
|
||||||
|
@ -9,7 +10,11 @@ import { Cache } from "@/misc/cache.js";
|
||||||
import { getWordHardMute } from "@/misc/check-word-mute.js";
|
import { getWordHardMute } from "@/misc/check-word-mute.js";
|
||||||
|
|
||||||
const blockingCache = new Cache<User["id"][]>("blocking", 60 * 5);
|
const blockingCache = new Cache<User["id"][]>("blocking", 60 * 5);
|
||||||
const mutedWordsCache = new Cache<string[][] | undefined>("mutedWords", 60 * 5);
|
const hardMutesCache = new Cache<{
|
||||||
|
userId: UserProfile["userId"];
|
||||||
|
mutedWords: UserProfile["mutedWords"];
|
||||||
|
mutedPatterns: UserProfile["mutedPatterns"];
|
||||||
|
}>("hardMutes", 60 * 5);
|
||||||
|
|
||||||
export async function checkHitAntenna(
|
export async function checkHitAntenna(
|
||||||
antenna: Antenna,
|
antenna: Antenna,
|
||||||
|
@ -89,12 +94,24 @@ export async function checkHitAntenna(
|
||||||
);
|
);
|
||||||
if (blockings.includes(antenna.userId)) return false;
|
if (blockings.includes(antenna.userId)) return false;
|
||||||
|
|
||||||
const mutedWords = await mutedWordsCache.fetch(antenna.userId, () =>
|
const mutes = await hardMutesCache.fetch(antenna.userId, () =>
|
||||||
UserProfiles.findOneBy({ userId: antenna.userId }).then(
|
UserProfiles.findOneByOrFail({
|
||||||
(profile) => profile?.mutedWords,
|
userId: antenna.userId,
|
||||||
),
|
}).then((profile) => {
|
||||||
|
return {
|
||||||
|
userId: antenna.userId,
|
||||||
|
mutedWords: profile.mutedWords,
|
||||||
|
mutedPatterns: profile.mutedPatterns,
|
||||||
|
};
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
if (await getWordHardMute(note, antenna.userId, mutedWords)) return false;
|
if (
|
||||||
|
mutes.mutedWords != null &&
|
||||||
|
mutes.mutedPatterns != null &&
|
||||||
|
antenna.userId !== note.userId &&
|
||||||
|
(await getWordHardMute(note, mutes.mutedWords, mutes.mutedPatterns))
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
// TODO: eval expression
|
// TODO: eval expression
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,14 @@ type NoteLike = {
|
||||||
text: Note["text"];
|
text: Note["text"];
|
||||||
files?: Note["files"];
|
files?: Note["files"];
|
||||||
cw?: Note["cw"];
|
cw?: Note["cw"];
|
||||||
|
reply?: NoteLike | null;
|
||||||
|
renote?: NoteLike | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkWordMute(
|
function checkWordMute(
|
||||||
note: NoteLike,
|
note: NoteLike | null | undefined,
|
||||||
mutedWords: Array<string | string[]>,
|
mutedWords: string[][],
|
||||||
|
mutedPatterns: string[],
|
||||||
): boolean {
|
): boolean {
|
||||||
if (note == null) return false;
|
if (note == null) return false;
|
||||||
|
|
||||||
|
@ -21,10 +24,9 @@ function checkWordMute(
|
||||||
|
|
||||||
if (text === "") return false;
|
if (text === "") return false;
|
||||||
|
|
||||||
for (const mutePattern of mutedWords) {
|
for (const mutedWord of mutedWords) {
|
||||||
if (Array.isArray(mutePattern)) {
|
|
||||||
// Clean up
|
// Clean up
|
||||||
const keywords = mutePattern.filter((keyword) => keyword !== "");
|
const keywords = mutedWord.filter((keyword) => keyword !== "");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
keywords.length > 0 &&
|
keywords.length > 0 &&
|
||||||
|
@ -33,13 +35,15 @@ function checkWordMute(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return true;
|
return true;
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
for (const mutedPattern of mutedPatterns) {
|
||||||
// represents RegExp
|
// represents RegExp
|
||||||
const regexp = mutePattern.match(/^\/(.+)\/(.*)$/);
|
const regexp = mutedPattern.match(/^\/(.+)\/(.*)$/);
|
||||||
|
|
||||||
// This should never happen due to input sanitisation.
|
// This should never happen due to input sanitisation.
|
||||||
if (!regexp) {
|
if (!regexp) {
|
||||||
console.warn(`Found invalid regex in word mutes: ${mutePattern}`);
|
console.warn(`Found invalid regex in word mutes: ${mutedPattern}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,23 +53,22 @@ function checkWordMute(
|
||||||
// This should never happen due to input sanitisation.
|
// This should never happen due to input sanitisation.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWordHardMute(
|
export async function getWordHardMute(
|
||||||
note: NoteLike,
|
note: NoteLike | null,
|
||||||
meId: string | null | undefined,
|
mutedWords: string[][],
|
||||||
mutedWords?: Array<string | string[]>,
|
mutedPatterns: string[],
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (note.userId === meId || mutedWords == null) return false;
|
if (note == null || mutedWords == null || mutedPatterns == null) return false;
|
||||||
|
|
||||||
if (mutedWords.length > 0) {
|
if (mutedWords.length > 0) {
|
||||||
return (
|
return (
|
||||||
checkWordMute(note, mutedWords) ||
|
checkWordMute(note, mutedWords, mutedPatterns) ||
|
||||||
checkWordMute(note.reply, mutedWords) ||
|
checkWordMute(note.reply, mutedWords, mutedPatterns) ||
|
||||||
checkWordMute(note.renote, mutedWords)
|
checkWordMute(note.renote, mutedWords, mutedPatterns)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -217,6 +217,12 @@ export class UserProfile {
|
||||||
})
|
})
|
||||||
public mutedWords: string[][];
|
public mutedWords: string[][];
|
||||||
|
|
||||||
|
@Column("text", {
|
||||||
|
array: true,
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
public mutedPatterns: string[];
|
||||||
|
|
||||||
@Column("jsonb", {
|
@Column("jsonb", {
|
||||||
default: [],
|
default: [],
|
||||||
comment: "List of instances muted by the user.",
|
comment: "List of instances muted by the user.",
|
||||||
|
|
|
@ -544,13 +544,13 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
? {
|
? {
|
||||||
avatarId: user.avatarId,
|
avatarId: user.avatarId,
|
||||||
bannerId: user.bannerId,
|
bannerId: user.bannerId,
|
||||||
injectFeaturedNote: profile!.injectFeaturedNote,
|
injectFeaturedNote: profile?.injectFeaturedNote,
|
||||||
receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
|
receiveAnnouncementEmail: profile?.receiveAnnouncementEmail,
|
||||||
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
|
alwaysMarkNsfw: profile?.alwaysMarkNsfw,
|
||||||
carefulBot: profile!.carefulBot,
|
carefulBot: profile?.carefulBot,
|
||||||
autoAcceptFollowed: profile!.autoAcceptFollowed,
|
autoAcceptFollowed: profile?.autoAcceptFollowed,
|
||||||
noCrawle: profile!.noCrawle,
|
noCrawle: profile?.noCrawle,
|
||||||
preventAiLearning: profile!.preventAiLearning,
|
preventAiLearning: profile?.preventAiLearning,
|
||||||
isExplorable: user.isExplorable,
|
isExplorable: user.isExplorable,
|
||||||
isDeleted: user.isDeleted,
|
isDeleted: user.isDeleted,
|
||||||
hideOnlineStatus: user.hideOnlineStatus,
|
hideOnlineStatus: user.hideOnlineStatus,
|
||||||
|
@ -571,17 +571,18 @@ export const UserRepository = db.getRepository(User).extend({
|
||||||
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
hasUnreadNotification: this.getHasUnreadNotification(user.id),
|
||||||
hasPendingReceivedFollowRequest:
|
hasPendingReceivedFollowRequest:
|
||||||
this.getHasPendingReceivedFollowRequest(user.id),
|
this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile?.mutedWords,
|
||||||
mutedInstances: profile!.mutedInstances,
|
mutedPatterns: profile?.mutedPatterns,
|
||||||
mutingNotificationTypes: profile!.mutingNotificationTypes,
|
mutedInstances: profile?.mutedInstances,
|
||||||
emailNotificationTypes: profile!.emailNotificationTypes,
|
mutingNotificationTypes: profile?.mutingNotificationTypes,
|
||||||
|
emailNotificationTypes: profile?.emailNotificationTypes,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
|
||||||
...(opts.includeSecrets
|
...(opts.includeSecrets
|
||||||
? {
|
? {
|
||||||
email: profile!.email,
|
email: profile?.email,
|
||||||
emailVerified: profile!.emailVerified,
|
emailVerified: profile?.emailVerified,
|
||||||
securityKeysList: UserSecurityKeys.find({
|
securityKeysList: UserSecurityKeys.find({
|
||||||
where: {
|
where: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
|
@ -468,6 +468,16 @@ export const packedMeDetailedOnlySchema = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mutedPatterns: {
|
||||||
|
type: "array",
|
||||||
|
nullable: false,
|
||||||
|
optional: false,
|
||||||
|
items: {
|
||||||
|
type: "string",
|
||||||
|
nullable: false,
|
||||||
|
optional: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
mutedInstances: {
|
mutedInstances: {
|
||||||
type: "array",
|
type: "array",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|
|
@ -60,6 +60,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
injectFeaturedNote: profile.injectFeaturedNote,
|
injectFeaturedNote: profile.injectFeaturedNote,
|
||||||
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
|
||||||
mutedWords: profile.mutedWords,
|
mutedWords: profile.mutedWords,
|
||||||
|
mutedPatterns: profile.mutedPatterns,
|
||||||
mutedInstances: profile.mutedInstances,
|
mutedInstances: profile.mutedInstances,
|
||||||
mutingNotificationTypes: profile.mutingNotificationTypes,
|
mutingNotificationTypes: profile.mutingNotificationTypes,
|
||||||
isModerator: user.isModerator,
|
isModerator: user.isModerator,
|
||||||
|
|
|
@ -120,6 +120,7 @@ export const paramDef = {
|
||||||
ffVisibility: { type: "string", enum: ["public", "followers", "private"] },
|
ffVisibility: { type: "string", enum: ["public", "followers", "private"] },
|
||||||
pinnedPageId: { type: "string", format: "misskey:id", nullable: true },
|
pinnedPageId: { type: "string", format: "misskey:id", nullable: true },
|
||||||
mutedWords: { type: "array" },
|
mutedWords: { type: "array" },
|
||||||
|
mutedPatterns: { type: "array", items: { type: "string" } },
|
||||||
mutedInstances: {
|
mutedInstances: {
|
||||||
type: "array",
|
type: "array",
|
||||||
items: {
|
items: {
|
||||||
|
@ -159,23 +160,52 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
||||||
profileUpdates.ffVisibility = ps.ffVisibility;
|
profileUpdates.ffVisibility = ps.ffVisibility;
|
||||||
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
|
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
|
||||||
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
|
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
|
||||||
if (ps.mutedWords !== undefined) {
|
if (ps.mutedPatterns !== undefined) {
|
||||||
// validate regular expression syntax
|
for (const item of ps.mutedPatterns) {
|
||||||
ps.mutedWords
|
const regexp = item.match(/^\/(.+)\/(.*)$/);
|
||||||
.filter((x) => !Array.isArray(x))
|
|
||||||
.forEach((x) => {
|
|
||||||
const regexp = x.match(/^\/(.+)\/(.*)$/);
|
|
||||||
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
|
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
new RE2(regexp[1], regexp[2]);
|
new RegExp(regexp[1], regexp[2]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new ApiError(meta.errors.invalidRegexp);
|
throw new ApiError(meta.errors.invalidRegexp);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
profileUpdates.mutedWords = ps.mutedWords;
|
profileUpdates.mutedPatterns = profileUpdates.mutedPatterns ?? [];
|
||||||
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
profileUpdates.mutedPatterns.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ps.mutedWords !== undefined) {
|
||||||
|
// for backward compatibility
|
||||||
|
for (const item of ps.mutedWords) {
|
||||||
|
if (Array.isArray(item)) continue;
|
||||||
|
|
||||||
|
const regexp = item.match(/^\/(.+)\/(.*)$/);
|
||||||
|
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
|
||||||
|
|
||||||
|
try {
|
||||||
|
new RegExp(regexp[1], regexp[2]);
|
||||||
|
} catch (err) {
|
||||||
|
throw new ApiError(meta.errors.invalidRegexp);
|
||||||
|
}
|
||||||
|
|
||||||
|
profileUpdates.mutedPatterns = profileUpdates.mutedPatterns ?? [];
|
||||||
|
profileUpdates.mutedPatterns.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
profileUpdates.mutedWords = ps.mutedWords.filter((item) =>
|
||||||
|
Array.isArray(item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
profileUpdates.mutedWords !== undefined ||
|
||||||
|
profileUpdates.mutedPatterns !== undefined
|
||||||
|
) {
|
||||||
|
profileUpdates.enableWordMute =
|
||||||
|
(profileUpdates.mutedWords != null &&
|
||||||
|
profileUpdates.mutedWords.length > 0) ||
|
||||||
|
(profileUpdates.mutedPatterns != null &&
|
||||||
|
profileUpdates.mutedPatterns.length > 0);
|
||||||
}
|
}
|
||||||
if (ps.mutedInstances !== undefined)
|
if (ps.mutedInstances !== undefined)
|
||||||
profileUpdates.mutedInstances = ps.mutedInstances;
|
profileUpdates.mutedInstances = ps.mutedInstances;
|
||||||
|
|
|
@ -69,7 +69,12 @@ export default class extends Channel {
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
|
this.user?.id !== note.userId &&
|
||||||
|
(await getWordHardMute(
|
||||||
|
note,
|
||||||
|
this.userProfile.mutedWords,
|
||||||
|
this.userProfile.mutedPatterns,
|
||||||
|
))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,12 @@ export default class extends Channel {
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
|
this.user?.id !== note.userId &&
|
||||||
|
(await getWordHardMute(
|
||||||
|
note,
|
||||||
|
this.userProfile.mutedWords,
|
||||||
|
this.userProfile.mutedPatterns,
|
||||||
|
))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,12 @@ export default class extends Channel {
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
|
this.user?.id !== note.userId &&
|
||||||
|
(await getWordHardMute(
|
||||||
|
note,
|
||||||
|
this.userProfile.mutedWords,
|
||||||
|
this.userProfile.mutedPatterns,
|
||||||
|
))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,12 @@ export default class extends Channel {
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
|
this.user?.id !== note.userId &&
|
||||||
|
(await getWordHardMute(
|
||||||
|
note,
|
||||||
|
this.userProfile.mutedWords,
|
||||||
|
this.userProfile.mutedPatterns,
|
||||||
|
))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,12 @@ export default class extends Channel {
|
||||||
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
// そのためレコードが存在するかのチェックでは不十分なので、改めてgetWordHardMuteを呼んでいる
|
||||||
if (
|
if (
|
||||||
this.userProfile &&
|
this.userProfile &&
|
||||||
(await getWordHardMute(note, this.user?.id, this.userProfile.mutedWords))
|
this.user?.id !== note.userId &&
|
||||||
|
(await getWordHardMute(
|
||||||
|
note,
|
||||||
|
this.userProfile.mutedWords,
|
||||||
|
this.userProfile.mutedPatterns,
|
||||||
|
))
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -65,9 +65,13 @@ import { inspect } from "node:util";
|
||||||
|
|
||||||
const logger = new Logger("create-note");
|
const logger = new Logger("create-note");
|
||||||
|
|
||||||
const mutedWordsCache = new Cache<
|
const hardMutesCache = new Cache<
|
||||||
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
|
{
|
||||||
>("mutedWords", 60 * 5);
|
userId: UserProfile["userId"];
|
||||||
|
mutedWords: UserProfile["mutedWords"];
|
||||||
|
mutedPatterns: UserProfile["mutedPatterns"];
|
||||||
|
}[]
|
||||||
|
>("hardMutes", 60 * 5);
|
||||||
|
|
||||||
type NotificationType = "reply" | "renote" | "quote" | "mention";
|
type NotificationType = "reply" | "renote" | "quote" | "mention";
|
||||||
|
|
||||||
|
@ -357,18 +361,20 @@ export default async (
|
||||||
incNotesCountOfUser(user);
|
incNotesCountOfUser(user);
|
||||||
|
|
||||||
// Word mute
|
// Word mute
|
||||||
mutedWordsCache
|
hardMutesCache
|
||||||
.fetch(null, () =>
|
.fetch(null, () =>
|
||||||
UserProfiles.find({
|
UserProfiles.find({
|
||||||
where: {
|
where: {
|
||||||
enableWordMute: true,
|
enableWordMute: true,
|
||||||
},
|
},
|
||||||
select: ["userId", "mutedWords"],
|
select: ["userId", "mutedWords", "mutedPatterns"],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then((us) => {
|
.then((us) => {
|
||||||
for (const u of us) {
|
for (const u of us) {
|
||||||
getWordHardMute(data, u.userId, u.mutedWords).then((shouldMute) => {
|
if (u.userId === user.id) return;
|
||||||
|
getWordHardMute(note, u.mutedWords, u.mutedPatterns).then(
|
||||||
|
(shouldMute: boolean) => {
|
||||||
if (shouldMute) {
|
if (shouldMute) {
|
||||||
MutedNotes.insert({
|
MutedNotes.insert({
|
||||||
id: genId(),
|
id: genId(),
|
||||||
|
@ -377,7 +383,8 @@ export default async (
|
||||||
reason: "word",
|
reason: "word",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -36,11 +36,15 @@
|
||||||
>
|
>
|
||||||
<FormTextarea v-model="hardMutedWords" class="_formBlock">
|
<FormTextarea v-model="hardMutedWords" class="_formBlock">
|
||||||
<span>{{ i18n.ts._wordMute.muteWords }}</span>
|
<span>{{ i18n.ts._wordMute.muteWords }}</span>
|
||||||
<template #caption
|
<template #caption>{{
|
||||||
>{{ i18n.ts._wordMute.muteWordsDescription }}<br />{{
|
i18n.ts._wordMute.muteWordsDescription
|
||||||
|
}}</template>
|
||||||
|
</FormTextarea>
|
||||||
|
<FormTextarea v-model="hardMutedPatterns" class="_formBlock">
|
||||||
|
<span>{{ i18n.ts._wordMute.mutePatterns }}</span>
|
||||||
|
<template #caption>{{
|
||||||
i18n.ts._wordMute.muteWordsDescription2
|
i18n.ts._wordMute.muteWordsDescription2
|
||||||
}}</template
|
}}</template>
|
||||||
>
|
|
||||||
</FormTextarea>
|
</FormTextarea>
|
||||||
<MkKeyValue
|
<MkKeyValue
|
||||||
v-if="hardWordMutedNotesCount != null"
|
v-if="hardWordMutedNotesCount != null"
|
||||||
|
@ -90,6 +94,7 @@ const tab = ref("soft");
|
||||||
const softMutedWords = ref(render(defaultStore.state.mutedWords));
|
const softMutedWords = ref(render(defaultStore.state.mutedWords));
|
||||||
const softMutedLangs = ref(render(defaultStore.state.mutedLangs));
|
const softMutedLangs = ref(render(defaultStore.state.mutedLangs));
|
||||||
const hardMutedWords = ref(render($i!.mutedWords));
|
const hardMutedWords = ref(render($i!.mutedWords));
|
||||||
|
const hardMutedPatterns = ref($i!.mutedPatterns.join("\n"));
|
||||||
const hardWordMutedNotesCount = ref(null);
|
const hardWordMutedNotesCount = ref(null);
|
||||||
const changed = ref(false);
|
const changed = ref(false);
|
||||||
|
|
||||||
|
@ -109,8 +114,12 @@ watch(hardMutedWords, () => {
|
||||||
changed.value = true;
|
changed.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(hardMutedPatterns, () => {
|
||||||
|
changed.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
const parseMutes = (mutes, tab) => {
|
const parseSoftMutes = (mutes, tab) => {
|
||||||
// split into lines, remove empty lines and unnecessary whitespace
|
// split into lines, remove empty lines and unnecessary whitespace
|
||||||
const lines = mutes
|
const lines = mutes
|
||||||
.trim()
|
.trim()
|
||||||
|
@ -151,11 +160,80 @@ async function save() {
|
||||||
return lines;
|
return lines;
|
||||||
};
|
};
|
||||||
|
|
||||||
let softMutes, softMLangs, hardMutes;
|
const parseMutedWords = (mutes) => {
|
||||||
|
// split into lines, remove empty lines and unnecessary whitespace
|
||||||
|
return mutes
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line !== "")
|
||||||
|
.map((line) => line.split(" "))
|
||||||
|
.filter((line) => line.length > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseMutedPatterns = (mutes, tab) => {
|
||||||
|
// split into lines, remove empty lines and unnecessary whitespace
|
||||||
|
const lines = mutes
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line !== "");
|
||||||
|
|
||||||
|
// check each line if it is a RegExp or not
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const regexp = line.match(/^\/(.+)\/(.*)$/);
|
||||||
|
if (regexp) {
|
||||||
|
// check that the RegExp is valid
|
||||||
try {
|
try {
|
||||||
softMutes = parseMutes(softMutedWords.value, i18n.ts._wordMute.soft);
|
new RegExp(regexp[1], regexp[2]);
|
||||||
softMLangs = parseMutes(softMutedLangs.value, i18n.ts._wordMute.lang);
|
// note that regex lines will not be split by spaces!
|
||||||
hardMutes = parseMutes(hardMutedWords.value, i18n.ts._wordMute.hard);
|
} catch (err: any) {
|
||||||
|
// invalid syntax: do not save, do not reset changed flag
|
||||||
|
os.alert({
|
||||||
|
type: "error",
|
||||||
|
title: i18n.ts.regexpError,
|
||||||
|
text:
|
||||||
|
i18n.t("regexpErrorDescription", {
|
||||||
|
tab,
|
||||||
|
line: i + 1,
|
||||||
|
}) +
|
||||||
|
"\n" +
|
||||||
|
err.toString(),
|
||||||
|
});
|
||||||
|
// re-throw error so these invalid settings are not saved
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// invalid syntax: do not save, do not reset changed flag
|
||||||
|
os.alert({
|
||||||
|
type: "error",
|
||||||
|
title: i18n.ts.regexpError,
|
||||||
|
text: i18n.t("regexpErrorDescription", {
|
||||||
|
tab,
|
||||||
|
line: i + 1,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
// re-throw error so these invalid settings are not saved
|
||||||
|
throw new Error("Invalid regular expression");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
let softMutes, softMLangs, hardMWords, hardMPatterns;
|
||||||
|
try {
|
||||||
|
softMutes = parseSoftMutes(
|
||||||
|
softMutedWords.value,
|
||||||
|
i18n.ts._wordMute.soft,
|
||||||
|
);
|
||||||
|
softMLangs = parseMutedWords(softMutedLangs.value);
|
||||||
|
hardMWords = parseMutedWords(hardMutedWords.value);
|
||||||
|
hardMPatterns = parseMutedPatterns(
|
||||||
|
hardMutedPatterns.value,
|
||||||
|
i18n.ts._wordMute.hard,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// already displayed error message in parseMutes
|
// already displayed error message in parseMutes
|
||||||
return;
|
return;
|
||||||
|
@ -164,7 +242,8 @@ async function save() {
|
||||||
defaultStore.set("mutedWords", softMutes);
|
defaultStore.set("mutedWords", softMutes);
|
||||||
defaultStore.set("mutedLangs", softMLangs);
|
defaultStore.set("mutedLangs", softMLangs);
|
||||||
await os.api("i/update", {
|
await os.api("i/update", {
|
||||||
mutedWords: hardMutes,
|
mutedWords: hardMWords,
|
||||||
|
mutedPatterns: hardMPatterns,
|
||||||
});
|
});
|
||||||
|
|
||||||
changed.value = false;
|
changed.value = false;
|
||||||
|
|
|
@ -107,6 +107,7 @@ export type MeDetailed = UserDetailed & {
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
isExplorable: boolean;
|
isExplorable: boolean;
|
||||||
mutedWords: string[][];
|
mutedWords: string[][];
|
||||||
|
mutedPatterns: string[];
|
||||||
mutingNotificationTypes: string[];
|
mutingNotificationTypes: string[];
|
||||||
noCrawle: boolean;
|
noCrawle: boolean;
|
||||||
preventAiLearning: boolean;
|
preventAiLearning: boolean;
|
||||||
|
|
Loading…
Reference in a new issue