From 5e53f9a8cf6ea49755849f4ec413d1473b2b8a57 Mon Sep 17 00:00:00 2001 From: naskya <m@naskya.net> Date: Fri, 17 May 2024 17:59:45 +0900 Subject: [PATCH] refactor (backend): port check-hit-antenna to backend-rs --- packages/backend-rs/index.d.ts | 21 +- packages/backend-rs/index.js | 4 +- packages/backend-rs/src/database/cache.rs | 6 + .../src/misc/add_note_to_antenna.rs | 31 --- .../backend-rs/src/misc/check_hit_antenna.rs | 203 ++++++++++++++++++ .../backend-rs/src/misc/check_word_mute.rs | 85 +------- .../backend-rs/src/misc/get_note_all_texts.rs | 79 +++++++ packages/backend-rs/src/misc/mod.rs | 3 +- packages/backend-rs/src/service/antenna.rs | 48 +++++ packages/backend-rs/src/service/mod.rs | 1 + .../backend/src/misc/check-hit-antenna.ts | 126 ----------- packages/backend/src/services/note/create.ts | 14 +- 12 files changed, 362 insertions(+), 259 deletions(-) delete mode 100644 packages/backend-rs/src/misc/add_note_to_antenna.rs create mode 100644 packages/backend-rs/src/misc/check_hit_antenna.rs create mode 100644 packages/backend-rs/src/misc/get_note_all_texts.rs create mode 100644 packages/backend-rs/src/service/antenna.rs delete mode 100644 packages/backend/src/misc/check-hit-antenna.ts diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 86c5044ac5..25ffd0b509 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -213,7 +213,6 @@ export function stringToAcct(acct: string): Acct export function acctToString(acct: Acct): string export function initializeRustLogger(): void export function showServerInfo(): void -export function addNoteToAntenna(antennaId: string, note: Note): void /** * Checks if a server is blocked. * @@ -236,15 +235,7 @@ export function isSilencedServer(host: string): Promise<boolean> * `host` - punycoded instance host */ export function isAllowedServer(host: string): Promise<boolean> -export interface NoteLikeForCheckWordMute { - fileIds: Array<string> - userId: string | null - text: string | null - cw: string | null - renoteId: string | null - replyId: string | null -} -export function checkWordMute(note: NoteLikeForCheckWordMute, mutedWords: Array<string>, mutedPatterns: Array<string>): Promise<boolean> +export function checkWordMute(note: NoteLike, mutedWords: Array<string>, mutedPatterns: Array<string>): Promise<boolean> export function getFullApAccount(username: string, host?: string | undefined | null): string export function isSelfHost(host?: string | undefined | null): boolean export function isSameOrigin(uri: string): boolean @@ -260,6 +251,15 @@ export interface ImageSize { height: number } export function getImageSizeFromUrl(url: string): Promise<ImageSize> +/** TODO: handle name collisions better */ +export interface NoteLikeForAllTexts { + fileIds: Array<string> + userId: string + text: string | null + cw: string | null + renoteId: string | null + replyId: string | null +} export interface NoteLikeForGetNoteSummary { fileIds: Array<string> text: string | null @@ -1175,6 +1175,7 @@ export interface Webhook { latestSentAt: Date | null latestStatus: number | null } +export function updateAntennaOnCreateNote(antenna: Antenna, note: Note, noteAuthor: Acct): Promise<void> export function fetchNodeinfo(host: string): Promise<Nodeinfo> export function nodeinfo_2_1(): Promise<any> export function nodeinfo_2_0(): Promise<any> diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 71247827a9..dd64d767ff 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { 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, addNoteToAntenna, 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, 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, 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 module.exports.SECOND = SECOND module.exports.MINUTE = MINUTE @@ -325,7 +325,6 @@ module.exports.stringToAcct = stringToAcct module.exports.acctToString = acctToString module.exports.initializeRustLogger = initializeRustLogger module.exports.showServerInfo = showServerInfo -module.exports.addNoteToAntenna = addNoteToAntenna module.exports.isBlockedServer = isBlockedServer module.exports.isSilencedServer = isSilencedServer module.exports.isAllowedServer = isAllowedServer @@ -370,6 +369,7 @@ module.exports.RelayStatusEnum = RelayStatusEnum module.exports.UserEmojimodpermEnum = UserEmojimodpermEnum module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum +module.exports.updateAntennaOnCreateNote = updateAntennaOnCreateNote module.exports.fetchNodeinfo = fetchNodeinfo module.exports.nodeinfo_2_1 = nodeinfo_2_1 module.exports.nodeinfo_2_0 = nodeinfo_2_0 diff --git a/packages/backend-rs/src/database/cache.rs b/packages/backend-rs/src/database/cache.rs index ba2910f254..f6f465ab00 100644 --- a/packages/backend-rs/src/database/cache.rs +++ b/packages/backend-rs/src/database/cache.rs @@ -6,6 +6,12 @@ use serde::{Deserialize, Serialize}; pub enum Category { #[strum(serialize = "fetchUrl")] FetchUrl, + #[strum(serialize = "blocking")] + Block, + #[strum(serialize = "following")] + Follow, + #[strum(serialize = "wordMute")] + WordMute, #[cfg(test)] #[strum(serialize = "usedOnlyForTesting")] Test, diff --git a/packages/backend-rs/src/misc/add_note_to_antenna.rs b/packages/backend-rs/src/misc/add_note_to_antenna.rs deleted file mode 100644 index 2ce4e655d7..0000000000 --- a/packages/backend-rs/src/misc/add_note_to_antenna.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::database::{redis_conn, redis_key}; -use crate::model::entity::note; -use crate::service::stream; -use crate::util::id::{get_timestamp, InvalidIdErr}; -use redis::{streams::StreamMaxlen, Commands, RedisError}; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("Redis error: {0}")] - RedisErr(#[from] RedisError), - #[error("Invalid ID: {0}")] - InvalidIdErr(#[from] InvalidIdErr), - #[error("Stream error: {0}")] - StreamErr(#[from] stream::Error), -} - -type Note = note::Model; - -#[crate::export] -pub fn add_note_to_antenna(antenna_id: String, note: &Note) -> Result<(), Error> { - // for timeline API - redis_conn()?.xadd_maxlen( - redis_key(format!("antennaTimeline:{}", antenna_id)), - StreamMaxlen::Approx(200), - format!("{}-*", get_timestamp(¬e.id)?), - &[("note", ¬e.id)], - )?; - - // for streaming API - Ok(stream::antenna::publish(antenna_id, note)?) -} diff --git a/packages/backend-rs/src/misc/check_hit_antenna.rs b/packages/backend-rs/src/misc/check_hit_antenna.rs new file mode 100644 index 0000000000..d8fa7b925f --- /dev/null +++ b/packages/backend-rs/src/misc/check_hit_antenna.rs @@ -0,0 +1,203 @@ +use crate::config::CONFIG; +use crate::database::{cache, db_conn}; +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::model::entity::{ + antenna, blocking, following, note, sea_orm_active_enums::*, user_profile, +}; +use sea_orm::{ColumnTrait, DbErr, EntityTrait, QueryFilter, QuerySelect}; + +#[derive(thiserror::Error, Debug)] +pub enum AntennaCheckError { + #[error("Database error: {0}")] + DbErr(#[from] DbErr), + #[error("Cache error: {0}")] + CacheErr(#[from] cache::Error), + #[error("User profile not found: {0}")] + UserProfileNotFoundErr(String), +} + +fn match_all(space_separated_words: &str, text: &str, case_sensitive: bool) -> bool { + if case_sensitive { + space_separated_words + .split_whitespace() + .all(|word| text.contains(word)) + } else { + space_separated_words + .to_lowercase() + .split_whitespace() + .all(|word| text.to_lowercase().contains(word)) + } +} + +pub async fn check_hit_antenna( + antenna: &antenna::Model, + note: note::Model, + note_author: &Acct, +) -> Result<bool, AntennaCheckError> { + if note.visibility == NoteVisibilityEnum::Specified { + return Ok(false); + } + + if antenna.with_file && note.file_ids.is_empty() { + return Ok(false); + } + + if !antenna.with_replies && note.reply_id.is_some() { + return Ok(false); + } + + if antenna.src == AntennaSrcEnum::Users { + let is_from_one_of_specified_authors = antenna + .users + .iter() + .map(|s| s.parse::<Acct>().unwrap()) + .any(|acct| acct.username == note_author.username && acct.host == note_author.host); + + if !is_from_one_of_specified_authors { + return Ok(false); + } + } else if antenna.src == AntennaSrcEnum::Instances { + let is_from_one_of_specified_servers = !antenna.instances.iter().any(|host| { + host.to_ascii_lowercase() + == note_author + .host + .clone() + .unwrap_or(CONFIG.host.clone()) + .to_ascii_lowercase() + }); + + if !is_from_one_of_specified_servers { + return Ok(false); + } + } + + // "Home", "Group", "List" sources are currently disabled + + let note_texts = all_texts(NoteLike { + file_ids: note.file_ids, + user_id: note.user_id.clone(), + text: note.text, + cw: note.cw, + renote_id: note.renote_id, + reply_id: note.reply_id, + }) + .await?; + + let has_keyword = antenna.keywords.iter().any(|words| { + note_texts + .iter() + .any(|text| match_all(words, text, antenna.case_sensitive)) + }); + + if !has_keyword { + return Ok(false); + } + + let has_excluded_word = antenna.exclude_keywords.iter().any(|words| { + note_texts + .iter() + .any(|text| match_all(words, text, antenna.case_sensitive)) + }); + + if has_excluded_word { + return Ok(false); + } + + let db = db_conn().await?; + + let blocked_user_ids: Vec<String> = cache::get_one(cache::Category::Block, ¬e.user_id)? + .unwrap_or({ + // cache miss + let blocks = blocking::Entity::find() + .select_only() + .column(blocking::Column::BlockeeId) + .filter(blocking::Column::BlockerId.eq(¬e.user_id)) + .into_tuple::<String>() + .all(db) + .await?; + cache::set_one(cache::Category::Block, ¬e.user_id, &blocks, 10 * 60)?; + blocks + }); + + // if the antenna owner is blocked by the note author, return false + if blocked_user_ids.contains(&antenna.user_id) { + return Ok(false); + } + + if [NoteVisibilityEnum::Home, NoteVisibilityEnum::Followers].contains(¬e.visibility) { + let following_user_ids: Vec<String> = + cache::get_one(cache::Category::Follow, &antenna.user_id)?.unwrap_or({ + // cache miss + let following = following::Entity::find() + .select_only() + .column(following::Column::FolloweeId) + .filter(following::Column::FollowerId.eq(&antenna.user_id)) + .into_tuple::<String>() + .all(db) + .await?; + cache::set_one( + cache::Category::Follow, + &antenna.user_id, + &following, + 10 * 60, + )?; + following + }); + + // if the antenna owner is not following the note author, return false + if !following_user_ids.contains(¬e.user_id) { + return Ok(false); + } + } + + 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(¬e_texts, &word_mute.0, &word_mute.1) { + return Ok(false); + } + + Ok(true) +} + +#[cfg(test)] +mod unit_test { + use super::match_all; + use pretty_assertions::assert_eq; + + #[test] + fn test_match_all() { + assert_eq!(match_all("Apple", "apple and banana", false), true); + assert_eq!(match_all("Apple", "apple and banana", true), false); + assert_eq!(match_all("Apple Banana", "apple and banana", false), true); + assert_eq!(match_all("Apple Banana", "apple and cinnamon", true), false); + assert_eq!( + match_all("Apple Banana", "apple and cinnamon", false), + false + ); + } +} diff --git a/packages/backend-rs/src/misc/check_word_mute.rs b/packages/backend-rs/src/misc/check_word_mute.rs index 571d0c6636..fd51765ac4 100644 --- a/packages/backend-rs/src/misc/check_word_mute.rs +++ b/packages/backend-rs/src/misc/check_word_mute.rs @@ -1,91 +1,14 @@ -use crate::database::db_conn; -use crate::model::entity::{drive_file, note}; +use crate::misc::get_note_all_texts::{all_texts, NoteLike}; use once_cell::sync::Lazy; use regex::Regex; -use sea_orm::{prelude::*, QuerySelect}; - -// TODO: handle name collisions in a better way -#[crate::export(object, js_name = "NoteLikeForCheckWordMute")] -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 all_texts(note: NoteLike) -> Result<Vec<String>, DbErr> { - let db = db_conn().await?; - - let mut texts: Vec<String> = vec![]; - - if let Some(text) = note.text { - texts.push(text); - } - if let Some(cw) = note.cw { - texts.push(cw); - } - - texts.extend( - drive_file::Entity::find() - .select_only() - .column(drive_file::Column::Comment) - .filter(drive_file::Column::Id.is_in(note.file_ids)) - .into_tuple::<Option<String>>() - .all(db) - .await? - .into_iter() - .flatten(), - ); - - if let Some(renote_id) = ¬e.renote_id { - if let Some((text, cw)) = note::Entity::find_by_id(renote_id) - .select_only() - .columns([note::Column::Text, note::Column::Cw]) - .into_tuple::<(Option<String>, Option<String>)>() - .one(db) - .await? - { - if let Some(t) = text { - texts.push(t); - } - if let Some(c) = cw { - texts.push(c); - } - } else { - tracing::warn!("nonexistent renote id: {:#?}", renote_id); - } - } - - if let Some(reply_id) = ¬e.reply_id { - if let Some((text, cw)) = note::Entity::find_by_id(reply_id) - .select_only() - .columns([note::Column::Text, note::Column::Cw]) - .into_tuple::<(Option<String>, Option<String>)>() - .one(db) - .await? - { - if let Some(t) = text { - texts.push(t); - } - if let Some(c) = cw { - texts.push(c); - } - } else { - tracing::warn!("nonexistent reply id: {:#?}", reply_id); - } - } - - Ok(texts) -} +use sea_orm::DbErr; fn convert_regex(js_regex: &str) -> String { static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^/(.+)/(.*)$").unwrap()); RE.replace(js_regex, "(?$2)$1").to_string() } -fn check_word_mute_impl( +pub fn check_word_mute_bare( texts: &[String], muted_words: &[String], muted_patterns: &[String], @@ -112,7 +35,7 @@ pub async fn check_word_mute( if muted_words.is_empty() && muted_patterns.is_empty() { Ok(false) } else { - Ok(check_word_mute_impl( + Ok(check_word_mute_bare( &all_texts(note).await?, muted_words, muted_patterns, diff --git a/packages/backend-rs/src/misc/get_note_all_texts.rs b/packages/backend-rs/src/misc/get_note_all_texts.rs new file mode 100644 index 0000000000..1e4bee396e --- /dev/null +++ b/packages/backend-rs/src/misc/get_note_all_texts.rs @@ -0,0 +1,79 @@ +use crate::database::db_conn; +use crate::model::entity::{drive_file, note}; +use sea_orm::{prelude::*, QuerySelect}; + +/// TODO: handle name collisions better +#[crate::export(object, js_name = "NoteLikeForAllTexts")] +pub struct NoteLike { + pub file_ids: Vec<String>, + pub user_id: String, + pub text: Option<String>, + pub cw: Option<String>, + pub renote_id: Option<String>, + pub reply_id: Option<String>, +} + +pub async fn all_texts(note: NoteLike) -> Result<Vec<String>, DbErr> { + let db = db_conn().await?; + + let mut texts: Vec<String> = vec![]; + + if let Some(text) = note.text { + texts.push(text); + } + if let Some(cw) = note.cw { + texts.push(cw); + } + + texts.extend( + drive_file::Entity::find() + .select_only() + .column(drive_file::Column::Comment) + .filter(drive_file::Column::Id.is_in(note.file_ids)) + .into_tuple::<Option<String>>() + .all(db) + .await? + .into_iter() + .flatten(), + ); + + if let Some(renote_id) = ¬e.renote_id { + if let Some((text, cw)) = note::Entity::find_by_id(renote_id) + .select_only() + .columns([note::Column::Text, note::Column::Cw]) + .into_tuple::<(Option<String>, Option<String>)>() + .one(db) + .await? + { + if let Some(t) = text { + texts.push(t); + } + if let Some(c) = cw { + texts.push(c); + } + } else { + tracing::warn!("nonexistent renote id: {:#?}", renote_id); + } + } + + if let Some(reply_id) = ¬e.reply_id { + if let Some((text, cw)) = note::Entity::find_by_id(reply_id) + .select_only() + .columns([note::Column::Text, note::Column::Cw]) + .into_tuple::<(Option<String>, Option<String>)>() + .one(db) + .await? + { + if let Some(t) = text { + texts.push(t); + } + if let Some(c) = cw { + texts.push(c); + } + } else { + tracing::warn!("nonexistent reply id: {:#?}", reply_id); + } + } + + Ok(texts) +} diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index ed84f4ece2..a91dc4c862 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -1,4 +1,4 @@ -pub mod add_note_to_antenna; +pub mod check_hit_antenna; pub mod check_server_block; pub mod check_word_mute; pub mod convert_host; @@ -6,6 +6,7 @@ pub mod emoji; pub mod escape_sql; pub mod format_milliseconds; pub mod get_image_size; +pub mod get_note_all_texts; pub mod get_note_summary; pub mod is_safe_url; pub mod latest_version; diff --git a/packages/backend-rs/src/service/antenna.rs b/packages/backend-rs/src/service/antenna.rs new file mode 100644 index 0000000000..582f2f541a --- /dev/null +++ b/packages/backend-rs/src/service/antenna.rs @@ -0,0 +1,48 @@ +use crate::database::{redis_conn, redis_key}; +use crate::federation::acct::Acct; +use crate::misc::check_hit_antenna::{check_hit_antenna, AntennaCheckError}; +use crate::model::entity::{antenna, note}; +use crate::service::stream; +use crate::util::id::{get_timestamp, InvalidIdErr}; +use redis::{streams::StreamMaxlen, Commands, RedisError}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Redis error: {0}")] + RedisErr(#[from] RedisError), + #[error("Invalid ID: {0}")] + InvalidIdErr(#[from] InvalidIdErr), + #[error("Stream error: {0}")] + StreamErr(#[from] stream::Error), + #[error("Failed to check if the note should be added to antenna: {0}")] + AntennaCheckErr(#[from] AntennaCheckError), +} + +// https://github.com/napi-rs/napi-rs/issues/2060 +type Antenna = antenna::Model; +type Note = note::Model; + +#[crate::export] +pub async fn update_antenna_on_create_note( + antenna: &Antenna, + note: Note, + note_author: &Acct, +) -> Result<(), Error> { + if check_hit_antenna(antenna, note.clone(), note_author).await? { + add_note_to_antenna(&antenna.id, ¬e)?; + } + Ok(()) +} + +pub fn add_note_to_antenna(antenna_id: &str, note: &Note) -> Result<(), Error> { + // for timeline API + redis_conn()?.xadd_maxlen( + redis_key(format!("antennaTimeline:{}", antenna_id)), + StreamMaxlen::Approx(200), + format!("{}-*", get_timestamp(¬e.id)?), + &[("note", ¬e.id)], + )?; + + // for streaming API + Ok(stream::antenna::publish(antenna_id.to_string(), note)?) +} diff --git a/packages/backend-rs/src/service/mod.rs b/packages/backend-rs/src/service/mod.rs index 6de98f4674..4a5a0a7311 100644 --- a/packages/backend-rs/src/service/mod.rs +++ b/packages/backend-rs/src/service/mod.rs @@ -1,3 +1,4 @@ +pub mod antenna; pub mod nodeinfo; pub mod note; pub mod push_notification; diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts deleted file mode 100644 index 1bdc6def79..0000000000 --- a/packages/backend/src/misc/check-hit-antenna.ts +++ /dev/null @@ -1,126 +0,0 @@ -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 { checkWordMute, getFullApAccount, stringToAcct } from "backend-rs"; -import type { Packed } from "@/misc/schema.js"; -import { Cache } from "@/misc/cache.js"; - -const blockingCache = new Cache<User["id"][]>("blocking", 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( - antenna: Antenna, - note: Note | Packed<"Note">, - noteUser: { id: User["id"]; username: string; host: string | null }, -): Promise<boolean> { - if (note.visibility === "specified") return false; - if (antenna.withFile) { - if (note.fileIds && note.fileIds.length === 0) return false; - } - if (!antenna.withReplies && note.replyId != null) return false; - - if (antenna.src === "users") { - const accts = antenna.users.map((x) => { - const { username, host } = stringToAcct(x); - return getFullApAccount(username, host).toLowerCase(); - }); - if ( - !accts.includes( - getFullApAccount(noteUser.username, noteUser.host).toLowerCase(), - ) - ) - return false; - } else if (antenna.src === "instances") { - const instances = antenna.instances - .filter((x) => x !== "") - .map((host) => { - return host.toLowerCase(); - }); - if (!instances.includes(noteUser.host?.toLowerCase() ?? "")) return false; - } - - let text = `${note.text ?? ""} ${note.cw ?? ""}`; - if (note.files != null) - text += ` ${note.files.map((f) => f.comment ?? "").join(" ")}`; - text = text.trim(); - - if (antenna.keywords.length > 0) { - if (note.text == null) return false; - - const matched = antenna.keywords.some((item) => - item - .split(" ") - .every((keyword) => - antenna.caseSensitive - ? text.includes(keyword) - : text.toLowerCase().includes(keyword.toLowerCase()), - ), - ); - - if (!matched) return false; - } - - if (antenna.excludeKeywords.length > 0) { - if (note.text == null) return false; - - const matched = antenna.excludeKeywords.some((item) => - item - .split(" ") - .every((keyword) => - antenna.caseSensitive - ? note.text?.includes(keyword) - : note.text?.toLowerCase().includes(keyword.toLowerCase()), - ), - ); - - if (matched) return false; - } - - // アンテナ作成者がノート作成者にブロックされていたらスキップ - const blockings = await blockingCache.fetch(noteUser.id, () => - Blockings.findBy({ blockerId: noteUser.id }).then((res) => - res.map((x) => x.blockeeId), - ), - ); - if (blockings.includes(antenna.userId)) return false; - - if (note.visibility === "followers" || note.visibility === "home") { - const following = await followingCache.fetch(antenna.userId, () => - Followings.find({ - where: { followerId: antenna.userId }, - select: ["followeeId"], - }).then((relations) => relations.map((relation) => relation.followeeId)), - ); - if (!following.includes(note.userId)) return false; - } - - 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 ( - mutes.mutedWords != null && - mutes.mutedPatterns != null && - antenna.userId !== note.userId && - (await checkWordMute(note, mutes.mutedWords, mutes.mutedPatterns)) - ) - return false; - - // TODO: eval expression - - return true; -} diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 0e484ffc3c..32c9647a14 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -42,9 +42,8 @@ import type { IPoll } from "@/models/entities/poll.js"; 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 { - addNoteToAntenna, + updateAntennaOnCreateNote, checkWordMute, genId, genIdAt, @@ -401,12 +400,11 @@ export default async ( // Antenna for (const antenna of await getAntennas()) { - checkHitAntenna(antenna, note, user).then((hit) => { - if (hit) { - // TODO: do this more sanely - addNoteToAntenna(antenna.id, toRustObject(note)); - } - }); + await updateAntennaOnCreateNote( + toRustObject(antenna), + toRustObject(note), + user, + ); } // Channel