refactor (backend): don't check word mute twice

This commit is contained in:
naskya 2024-05-17 17:59:45 +09:00
parent 5e53f9a8cf
commit ffa08748d0
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
10 changed files with 62 additions and 92 deletions

View file

@ -267,6 +267,7 @@ export interface NoteLikeForGetNoteSummary {
hasPoll: boolean hasPoll: boolean
} }
export function getNoteSummary(note: NoteLikeForGetNoteSummary): string export function getNoteSummary(note: NoteLikeForGetNoteSummary): string
export function isQuote(note: Note): boolean
export function isSafeUrl(url: string): boolean export function isSafeUrl(url: string): boolean
export function latestVersion(): Promise<string> export function latestVersion(): Promise<string>
export function toMastodonId(firefishId: string): string | null export function toMastodonId(firefishId: string): string | null
@ -1175,7 +1176,7 @@ export interface Webhook {
latestSentAt: Date | null latestSentAt: Date | null
latestStatus: number | null latestStatus: number | null
} }
export function updateAntennaOnCreateNote(antenna: Antenna, note: Note, noteAuthor: Acct): Promise<void> export function updateAntennasOnNewNote(note: Note, noteAuthor: Acct, noteMutedUsers: Array<string>): Promise<void>
export function fetchNodeinfo(host: string): Promise<Nodeinfo> export function fetchNodeinfo(host: string): Promise<Nodeinfo>
export function nodeinfo_2_1(): Promise<any> export function nodeinfo_2_1(): Promise<any>
export function nodeinfo_2_0(): Promise<any> export function nodeinfo_2_0(): Promise<any>

View file

@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`) throw new Error(`Failed to load native binding`)
} }
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, updateAntennaOnCreateNote, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isQuote, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, updateAntennasOnNewNote, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding
module.exports.SECOND = SECOND module.exports.SECOND = SECOND
module.exports.MINUTE = MINUTE module.exports.MINUTE = MINUTE
@ -340,6 +340,7 @@ module.exports.safeForSql = safeForSql
module.exports.formatMilliseconds = formatMilliseconds module.exports.formatMilliseconds = formatMilliseconds
module.exports.getImageSizeFromUrl = getImageSizeFromUrl module.exports.getImageSizeFromUrl = getImageSizeFromUrl
module.exports.getNoteSummary = getNoteSummary module.exports.getNoteSummary = getNoteSummary
module.exports.isQuote = isQuote
module.exports.isSafeUrl = isSafeUrl module.exports.isSafeUrl = isSafeUrl
module.exports.latestVersion = latestVersion module.exports.latestVersion = latestVersion
module.exports.toMastodonId = toMastodonId module.exports.toMastodonId = toMastodonId
@ -369,7 +370,7 @@ module.exports.RelayStatusEnum = RelayStatusEnum
module.exports.UserEmojimodpermEnum = UserEmojimodpermEnum module.exports.UserEmojimodpermEnum = UserEmojimodpermEnum
module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum
module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
module.exports.updateAntennaOnCreateNote = updateAntennaOnCreateNote module.exports.updateAntennasOnNewNote = updateAntennasOnNewNote
module.exports.fetchNodeinfo = fetchNodeinfo module.exports.fetchNodeinfo = fetchNodeinfo
module.exports.nodeinfo_2_1 = nodeinfo_2_1 module.exports.nodeinfo_2_1 = nodeinfo_2_1
module.exports.nodeinfo_2_0 = nodeinfo_2_0 module.exports.nodeinfo_2_0 = nodeinfo_2_0

View file

@ -10,8 +10,6 @@ pub enum Category {
Block, Block,
#[strum(serialize = "following")] #[strum(serialize = "following")]
Follow, Follow,
#[strum(serialize = "wordMute")]
WordMute,
#[cfg(test)] #[cfg(test)]
#[strum(serialize = "usedOnlyForTesting")] #[strum(serialize = "usedOnlyForTesting")]
Test, Test,

View file

@ -1,11 +1,8 @@
use crate::config::CONFIG; use crate::config::CONFIG;
use crate::database::{cache, db_conn}; use crate::database::{cache, db_conn};
use crate::federation::acct::Acct; use crate::federation::acct::Acct;
use crate::misc::check_word_mute::check_word_mute_bare;
use crate::misc::get_note_all_texts::{all_texts, NoteLike}; use crate::misc::get_note_all_texts::{all_texts, NoteLike};
use crate::model::entity::{ use crate::model::entity::{antenna, blocking, following, note, sea_orm_active_enums::*};
antenna, blocking, following, note, sea_orm_active_enums::*, user_profile,
};
use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect}; use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect};
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -152,35 +149,6 @@ pub async fn check_hit_antenna(
} }
} }
type WordMute = (
Vec<String>, // muted words
Vec<String>, // muted patterns
);
let word_mute: WordMute = cache::get_one(cache::Category::WordMute, &antenna.user_id)?
.unwrap_or({
// cache miss
let mute = user_profile::Entity::find()
.select_only()
.columns([
user_profile::Column::MutedWords,
user_profile::Column::MutedPatterns,
])
.into_tuple::<WordMute>()
.one(db)
.await?
.ok_or({
tracing::warn!("there is no user_profile for user {}", &antenna.user_id);
AntennaCheckError::UserProfileNotFoundErr(antenna.user_id.clone())
})?;
cache::set_one(cache::Category::WordMute, &antenna.user_id, &mute, 10 * 60)?;
mute
});
if check_word_mute_bare(&note_texts, &word_mute.0, &word_mute.1) {
return Ok(false);
}
Ok(true) Ok(true)
} }

View file

@ -8,7 +8,7 @@ fn convert_regex(js_regex: &str) -> String {
RE.replace(js_regex, "(?$2)$1").to_string() RE.replace(js_regex, "(?$2)$1").to_string()
} }
pub fn check_word_mute_bare( fn check_word_mute_impl(
texts: &[String], texts: &[String],
muted_words: &[String], muted_words: &[String],
muted_patterns: &[String], muted_patterns: &[String],
@ -35,7 +35,7 @@ pub async fn check_word_mute(
if muted_words.is_empty() && muted_patterns.is_empty() { if muted_words.is_empty() && muted_patterns.is_empty() {
Ok(false) Ok(false)
} else { } else {
Ok(check_word_mute_bare( Ok(check_word_mute_impl(
&all_texts(note).await?, &all_texts(note).await?,
muted_words, muted_words,
muted_patterns, muted_patterns,

View file

@ -0,0 +1,9 @@
use crate::model::entity::note;
// https://github.com/napi-rs/napi-rs/issues/2060
type Note = note::Model;
#[crate::export]
pub fn is_quote(note: Note) -> bool {
note.renote_id.is_some() && (note.text.is_some() || note.has_poll || !note.file_ids.is_empty())
}

View file

@ -8,6 +8,7 @@ pub mod format_milliseconds;
pub mod get_image_size; pub mod get_image_size;
pub mod get_note_all_texts; pub mod get_note_all_texts;
pub mod get_note_summary; pub mod get_note_summary;
pub mod is_quote;
pub mod is_safe_url; pub mod is_safe_url;
pub mod latest_version; pub mod latest_version;
pub mod mastodon_id; pub mod mastodon_id;

View file

@ -1,13 +1,19 @@
use crate::database::{redis_conn, redis_key}; use crate::database::cache;
use crate::database::{db_conn, redis_conn, redis_key};
use crate::federation::acct::Acct; use crate::federation::acct::Acct;
use crate::misc::check_hit_antenna::{check_hit_antenna, AntennaCheckError}; use crate::misc::check_hit_antenna::{check_hit_antenna, AntennaCheckError};
use crate::model::entity::{antenna, note}; use crate::model::entity::{antenna, note};
use crate::service::stream; use crate::service::stream;
use crate::util::id::{get_timestamp, InvalidIdErr}; use crate::util::id::{get_timestamp, InvalidIdErr};
use redis::{streams::StreamMaxlen, Commands, RedisError}; use redis::{streams::StreamMaxlen, Commands, RedisError};
use sea_orm::{DbErr, EntityTrait};
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
#[error("Database error: {0}")]
DbErr(#[from] DbErr),
#[error("Cache error: {0}")]
CacheErr(#[from] cache::Error),
#[error("Redis error: {0}")] #[error("Redis error: {0}")]
RedisErr(#[from] RedisError), RedisErr(#[from] RedisError),
#[error("Invalid ID: {0}")] #[error("Invalid ID: {0}")]
@ -22,15 +28,33 @@ pub enum Error {
type Antenna = antenna::Model; type Antenna = antenna::Model;
type Note = note::Model; type Note = note::Model;
// TODO?: it might be better to store this directly in memory
// (like fetch_meta) instead of Redis as it's used so much
async fn antennas() -> Result<Vec<Antenna>, Error> {
const CACHE_KEY: &str = "antennas";
Ok(cache::get::<Vec<Antenna>>(CACHE_KEY)?.unwrap_or({
let antennas = antenna::Entity::find().all(db_conn().await?).await?;
cache::set(CACHE_KEY, &antennas, 5 * 60)?;
antennas
}))
}
#[crate::export] #[crate::export]
pub async fn update_antenna_on_create_note( pub async fn update_antennas_on_new_note(
antenna: &Antenna,
note: Note, note: Note,
note_author: &Acct, note_author: &Acct,
note_muted_users: Vec<String>,
) -> Result<(), Error> { ) -> Result<(), Error> {
for antenna in antennas().await?.iter() {
if note_muted_users.contains(&antenna.user_id) {
continue;
}
if check_hit_antenna(antenna, note.clone(), note_author).await? { if check_hit_antenna(antenna, note.clone(), note_author).await? {
add_note_to_antenna(&antenna.id, &note)?; add_note_to_antenna(&antenna.id, &note)?;
} }
}
Ok(()) Ok(())
} }

View file

@ -1,36 +0,0 @@
import { Antennas } from "@/models/index.js";
import type { Antenna } from "@/models/entities/antenna.js";
import { subscriber } from "@/db/redis.js";
let antennasFetched = false;
let antennas: Antenna[] = [];
export async function getAntennas() {
if (!antennasFetched) {
antennas = await Antennas.find();
antennasFetched = true;
}
return antennas;
}
subscriber.on("message", async (_, data) => {
const obj = JSON.parse(data);
if (obj.channel === "internal") {
const { type, body } = obj.message;
switch (type) {
case "antennaCreated":
antennas.push(body);
break;
case "antennaUpdated":
antennas[antennas.findIndex((a) => a.id === body.id)] = body;
break;
case "antennaDeleted":
antennas = antennas.filter((a) => a.id !== body.id);
break;
default:
break;
}
}
});

View file

@ -43,17 +43,17 @@ import { Poll } from "@/models/entities/poll.js";
import { createNotification } from "@/services/create-notification.js"; import { createNotification } from "@/services/create-notification.js";
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js"; import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
import { import {
updateAntennaOnCreateNote, updateAntennasOnNewNote,
checkWordMute, checkWordMute,
genId, genId,
genIdAt, genIdAt,
isQuote,
isSilencedServer, isSilencedServer,
} from "backend-rs"; } from "backend-rs";
import { countSameRenotes } from "@/misc/count-same-renotes.js"; import { countSameRenotes } from "@/misc/count-same-renotes.js";
import { deliverToRelays, getCachedRelays } from "../relay.js"; import { deliverToRelays, getCachedRelays } from "../relay.js";
import type { Channel } from "@/models/entities/channel.js"; import type { Channel } from "@/models/entities/channel.js";
import { normalizeForSearch } from "@/misc/normalize-for-search.js"; import { normalizeForSearch } from "@/misc/normalize-for-search.js";
import { getAntennas } from "@/misc/antenna-cache.js";
import { endedPollNotificationQueue } from "@/queue/queues.js"; import { endedPollNotificationQueue } from "@/queue/queues.js";
import { webhookDeliver } from "@/queue/index.js"; import { webhookDeliver } from "@/queue/index.js";
import { Cache } from "@/misc/cache.js"; import { Cache } from "@/misc/cache.js";
@ -370,8 +370,10 @@ export default async (
// Increment notes count (user) // Increment notes count (user)
incNotesCountOfUser(user); incNotesCountOfUser(user);
// Word mute // Word mutes & antenna
hardMutesCache const thisNoteIsMutedBy: string[] = [];
await hardMutesCache
.fetch(null, () => .fetch(null, () =>
UserProfiles.find({ UserProfiles.find({
where: { where: {
@ -380,12 +382,13 @@ export default async (
select: ["userId", "mutedWords", "mutedPatterns"], select: ["userId", "mutedWords", "mutedPatterns"],
}), }),
) )
.then((us) => { .then(async (us) => {
for (const u of us) { for (const u of us) {
if (u.userId === user.id) return; if (u.userId === user.id) return;
checkWordMute(note, u.mutedWords, u.mutedPatterns).then( await checkWordMute(note, u.mutedWords, u.mutedPatterns).then(
(shouldMute: boolean) => { (shouldMute: boolean) => {
if (shouldMute) { if (shouldMute) {
thisNoteIsMutedBy.push(u.userId);
MutedNotes.insert({ MutedNotes.insert({
id: genId(), id: genId(),
userId: u.userId, userId: u.userId,
@ -398,12 +401,13 @@ export default async (
} }
}); });
// Antenna // type errors will be resolved by https://github.com/napi-rs/napi-rs/pull/2054
for (const antenna of await getAntennas()) { const _note = toRustObject(note)
await updateAntennaOnCreateNote( if (note.renoteId == null || isQuote(_note)) {
toRustObject(antenna), await updateAntennasOnNewNote(
toRustObject(note), _note,
user, user,
thisNoteIsMutedBy,
); );
} }