diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 98f5aab2e0..b7737cd3fb 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -378,6 +378,13 @@ export function isSilencedServer(host: string): Promise * ``` */ export function isAllowedServer(host: string): Promise +export interface PartialNoteToCheckWordMute { + fileIds: Array + text: string | null + cw: string | null + renoteId: string | null + replyId: string | null +} /** * Returns whether `note` should be hard-muted. * @@ -390,11 +397,11 @@ export function isAllowedServer(host: string): Promise * * # Arguments * - * * `note` : [PartialNoteToElaborate] object + * * `note` : [PartialNoteToCheckWordMute] object * * `muted_words` : list of muted keyword lists (each array item is a space-separated keyword list that represents an AND condition) * * `muted_patterns` : list of JavaScript-style (e.g., `/foo/i`) regular expressions */ -export function checkWordMute(note: PartialNoteToElaborate, mutedWords: Array, mutedPatterns: Array): Promise +export function checkWordMute(note: PartialNoteToCheckWordMute, mutedWords: Array, mutedPatterns: Array): Promise export function getFullApAccount(username: string, host?: string | undefined | null): string export function isSelfHost(host?: string | undefined | null): boolean export function isSameOrigin(uri: string): boolean @@ -413,14 +420,6 @@ export interface ImageSize { height: number } export function getImageSizeFromUrl(url: string): Promise -export interface PartialNoteToElaborate { - fileIds: Array - userId: string - text: string | null - cw: string | null - renoteId: string | null - replyId: string | null -} export interface PartialNoteToSummarize { fileIds: Array text: string | null diff --git a/packages/backend-rs/src/misc/check_word_mute.rs b/packages/backend-rs/src/misc/check_word_mute.rs index 01ac03212e..6faf9352ec 100644 --- a/packages/backend-rs/src/misc/check_word_mute.rs +++ b/packages/backend-rs/src/misc/check_word_mute.rs @@ -1,8 +1,17 @@ -use crate::misc::get_note_all_texts::{all_texts, PartialNoteToElaborate}; +use crate::misc::get_note_all_texts::all_texts; use once_cell::sync::Lazy; use regex::Regex; use sea_orm::DbErr; +#[crate::export(object)] +pub struct PartialNoteToCheckWordMute { + pub file_ids: Vec, + pub text: Option, + pub cw: Option, + pub renote_id: Option, + pub reply_id: Option, +} + fn convert_regex(js_regex: &str) -> String { static RE: Lazy = Lazy::new(|| Regex::new(r"^/(.+)/(.*)$").unwrap()); RE.replace(js_regex, "(?$2)$1").to_string() @@ -37,12 +46,12 @@ fn check_word_mute_impl( /// /// # Arguments /// -/// * `note` : [PartialNoteToElaborate] object +/// * `note` : [PartialNoteToCheckWordMute] object /// * `muted_words` : list of muted keyword lists (each array item is a space-separated keyword list that represents an AND condition) /// * `muted_patterns` : list of JavaScript-style (e.g., `/foo/i`) regular expressions #[crate::export] pub async fn check_word_mute( - note: PartialNoteToElaborate, + note: PartialNoteToCheckWordMute, muted_words: &[String], muted_patterns: &[String], ) -> Result { @@ -50,7 +59,7 @@ pub async fn check_word_mute( Ok(false) } else { Ok(check_word_mute_impl( - &all_texts(note, true).await?, + &all_texts!(note, true).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 index 0532c48054..8db5fbc8f3 100644 --- a/packages/backend-rs/src/misc/get_note_all_texts.rs +++ b/packages/backend-rs/src/misc/get_note_all_texts.rs @@ -4,40 +4,33 @@ use crate::{ }; use sea_orm::{prelude::*, QuerySelect}; -#[crate::export(object)] -pub struct PartialNoteToElaborate { - pub file_ids: Vec, - pub user_id: String, - pub text: Option, - pub cw: Option, - pub renote_id: Option, - pub reply_id: Option, -} - /// Returns [`Vec`] containing the post text, content warning, /// those of the "parent" (replied/quoted) posts, and alt texts of attached files. +/// Consider using [`all_texts`] macro instead +/// when dealing with a note ([`note::Model`])-like instance. /// /// # Arguments /// -/// * `note` : [PartialNoteToElaborate] object +/// * `file_ids` : IDs of attached files ([`drive_file::Model`]) +/// * `text`, `cw`, `renote_id`, `reply_id` : note ([`note::Model`]) fields /// * `include_parent` : whether to take the reply-to post and quoted post into account -pub async fn all_texts( - note: PartialNoteToElaborate, +pub async fn all_texts_impl( + file_ids: &[String], + text: Option, + cw: Option, + renote_id: &Option, + reply_id: &Option, include_parent: bool, ) -> Result, DbErr> { let db = db_conn().await?; let mut texts: Vec = vec![]; - let is_renote: bool; + let is_renote = text.is_none(); - if let Some(text) = note.text { - is_renote = false; + if let Some(text) = text { texts.push(text); - } else { - is_renote = true; } - - if let Some(cw) = note.cw { + if let Some(cw) = cw { texts.push(cw); } @@ -45,7 +38,7 @@ pub async fn all_texts( drive_file::Entity::find() .select_only() .column(drive_file::Column::Comment) - .filter(drive_file::Column::Id.is_in(note.file_ids)) + .filter(drive_file::Column::Id.is_in(file_ids)) .into_tuple::>() .all(db) .await? @@ -53,45 +46,87 @@ pub async fn all_texts( .flatten(), ); - if note.renote_id.is_some() && (include_parent || is_renote) { - let renote_id = note.renote_id.unwrap(); - - if let Some((text, cw)) = note::Entity::find_by_id(&renote_id) - .select_only() - .columns([note::Column::Text, note::Column::Cw]) - .into_tuple::<(Option, Option)>() - .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); + let mut query_note_ids = Vec::<&str>::with_capacity(2); + if let Some(renote_id) = renote_id { + if include_parent || is_renote { + query_note_ids.push(renote_id); } } - - if include_parent && note.reply_id.is_some() { - if let Some((text, cw)) = note::Entity::find_by_id(note.reply_id.as_ref().unwrap()) - .select_only() - .columns([note::Column::Text, note::Column::Cw]) - .into_tuple::<(Option, Option)>() - .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: {}", note.reply_id.unwrap()); + if let Some(reply_id) = reply_id { + if include_parent { + query_note_ids.push(reply_id); } } + if !query_note_ids.is_empty() { + texts.extend( + note::Entity::find() + .filter(note::Column::Id.is_in(query_note_ids)) + .select_only() + .columns([note::Column::Text, note::Column::Cw]) + .into_tuple::<(Option, Option)>() + .one(db) + .await? + .into_iter() + .flat_map(|(text, cw)| [text, cw]) + .flatten(), + ); + } Ok(texts) } + +/// Returns [`Vec`] containing the post text, content warning, +/// those of the "parent" (replied/quoted) posts, and alt texts of attached files. +/// +/// # Arguments +/// +/// * `note_like` : a note ([`note::Model`])-like instance containing +/// `file_ids`, `text`, `cw`, `renote_id`, `reply_id` fields +/// * `include_parent` ([bool]) : whether to take the reply-to post and quoted post into account +/// +/// # Caveats +/// +/// The `note_like` argument should not contain function calls +/// (e.g., `all_texts!(note.clone(), false)`) +/// since the function will be called multiple times after macro expansion. +/// +/// # Examples +/// +/// ``` +/// # use backend_rs::misc::get_note_all_texts::all_texts; +/// // note-like struct +/// struct SomeNoteLikeStruct { +/// // required fields +/// file_ids: Vec, +/// text: Option, +/// cw: Option, +/// renote_id: Option, +/// reply_id: Option, +/// // arbitrary extra fields +/// extra_field_1: u32, +/// extra_field_2: Vec, +/// } +/// +/// async fn all_texts_from_some_note_like_struct( +/// note_like: &SomeNoteLikeStruct, +/// include_parent: bool, +/// ) -> Result, sea_orm::DbErr> { +/// all_texts!(note_like, include_parent).await +/// } +/// ``` +#[doc(hidden)] // hide the macro in the top doc page +#[macro_export] +macro_rules! all_texts { + ($note_like:expr, $include_parent:expr) => { + $crate::misc::get_note_all_texts::all_texts_impl( + &$note_like.file_ids, + $note_like.text.clone(), + $note_like.cw.clone(), + &$note_like.renote_id, + &$note_like.reply_id, + $include_parent, + ) + }; +} +#[doc(inline)] // show the macro in the module doc page +pub use all_texts; diff --git a/packages/backend-rs/src/service/antenna/cache.rs b/packages/backend-rs/src/service/antenna/cache.rs index 65bd6efca9..ce83895368 100644 --- a/packages/backend-rs/src/service/antenna/cache.rs +++ b/packages/backend-rs/src/service/antenna/cache.rs @@ -2,24 +2,23 @@ use crate::{database::db_conn, model::entity::antenna}; use sea_orm::prelude::*; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; -static CACHE: Mutex>> = Mutex::new(None); +static CACHE: Mutex>> = Mutex::new(None); -fn set(antennas: &[antenna::Model]) { - let _ = CACHE - .lock() - .map(|mut cache| *cache = Some(antennas.to_owned())); +fn set(antennas: Arc<[antenna::Model]>) { + let _ = CACHE.lock().map(|mut cache| *cache = Some(antennas)); } -pub(super) async fn update() -> Result, DbErr> { +pub(super) async fn update() -> Result, DbErr> { tracing::debug!("updating cache"); - let antennas = antenna::Entity::find().all(db_conn().await?).await?; - set(&antennas); + let antennas: Arc<[antenna::Model]> = + antenna::Entity::find().all(db_conn().await?).await?.into(); + set(antennas.clone()); Ok(antennas) } -pub(super) async fn get() -> Result, DbErr> { +pub(super) async fn get() -> Result, DbErr> { if let Some(cache) = CACHE.lock().ok().and_then(|cache| cache.clone()) { return Ok(cache); } diff --git a/packages/backend-rs/src/service/antenna/check_hit.rs b/packages/backend-rs/src/service/antenna/check_hit.rs index 0f297ccbb2..fff8962ed4 100644 --- a/packages/backend-rs/src/service/antenna/check_hit.rs +++ b/packages/backend-rs/src/service/antenna/check_hit.rs @@ -12,8 +12,6 @@ pub enum AntennaCheckError { Db(#[from] DbErr), #[error("Cache error: {0}")] Cache(#[from] cache::Error), - #[error("User profile not found: {0}")] - UserProfileNotFound(String), } fn match_all(space_separated_words: &str, text: &str, case_sensitive: bool) -> bool { @@ -29,7 +27,7 @@ fn match_all(space_separated_words: &str, text: &str, case_sensitive: bool) -> b } } -pub async fn check_hit_antenna( +pub(super) async fn check_hit_antenna( antenna: &antenna::Model, note: ¬e::Model, note_all_texts: &[String], diff --git a/packages/backend-rs/src/service/antenna/process_new_note.rs b/packages/backend-rs/src/service/antenna/process_new_note.rs index b9eed0eff8..f4ec336681 100644 --- a/packages/backend-rs/src/service/antenna/process_new_note.rs +++ b/packages/backend-rs/src/service/antenna/process_new_note.rs @@ -1,7 +1,7 @@ use crate::{ database::{cache, redis_conn, redis_key, RedisConnError}, federation::acct::Acct, - misc::get_note_all_texts::{all_texts, PartialNoteToElaborate}, + misc::get_note_all_texts::all_texts, model::entity::note, service::{ antenna, @@ -37,31 +37,19 @@ type Note = note::Model; #[crate::export] pub async fn update_antennas_on_new_note( - note: Note, + note: &Note, note_author: &Acct, note_muted_users: &[String], ) -> Result<(), Error> { - let note_cloned = note.clone(); - let note_all_texts = all_texts( - PartialNoteToElaborate { - file_ids: note.file_ids, - user_id: note.user_id, - text: note.text, - cw: note.cw, - renote_id: note.renote_id, - reply_id: note.reply_id, - }, - false, - ) - .await?; + let note_all_texts = all_texts!(note, false).await?; // TODO: do this in parallel for antenna in antenna::cache::get().await?.iter() { if note_muted_users.contains(&antenna.user_id) { continue; } - if check_hit_antenna(antenna, ¬e_cloned, ¬e_all_texts, note_author).await? { - add_note_to_antenna(&antenna.id, ¬e_cloned).await?; + if check_hit_antenna(antenna, note, ¬e_all_texts, note_author).await? { + add_note_to_antenna(&antenna.id, note).await?; } } diff --git a/packages/client/package.json b/packages/client/package.json index bcfdc60520..3573c27ba1 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -90,6 +90,6 @@ "vue-draggable-plus": "0.5.0", "vue-plyr": "7.0.0", "vue-prism-editor": "2.0.0-alpha.2", - "vue-tsc": "2.0.19" + "vue-tsc": "2.0.21" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 736b7928b2..e1261b4166 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -762,8 +762,8 @@ importers: specifier: 2.0.0-alpha.2 version: 2.0.0-alpha.2(vue@3.4.27(typescript@5.4.5)) vue-tsc: - specifier: 2.0.19 - version: 2.0.19(typescript@5.4.5) + specifier: 2.0.21 + version: 2.0.21(typescript@5.4.5) packages/firefish-js: dependencies: @@ -2586,14 +2586,14 @@ packages: vite: ^5.0.0 vue: ^3.2.25 - '@volar/language-core@2.2.5': - resolution: {integrity: sha512-2htyAuxRrAgETmFeUhT4XLELk3LiEcqoW/B8YUXMF6BrGWLMwIR09MFaZYvrA2UhbdAeSyeQ726HaWSWkexUcQ==} + '@volar/language-core@2.3.0-alpha.15': + resolution: {integrity: sha512-uSfn1Dsl1w9o2aN9nnS6N/4FcjSbmpY6P/ypfW4kRhasEyICstu4swIz2joNR6532R02JwJY9Ta0pxRmXbBOqw==} - '@volar/source-map@2.2.5': - resolution: {integrity: sha512-wrOEIiZNf4E+PWB0AxyM4tfhkfldPsb3bxg8N6FHrxJH2ohar7aGu48e98bp3pR9HUA7P/pR9VrLmkTrgCCnWQ==} + '@volar/source-map@2.3.0-alpha.15': + resolution: {integrity: sha512-DQr3FwhRxtxX4W6BoJkwajWjj6BAF5H/SgtzFaUP9z8txn6Y5oFxZPPDG+3Xwu3pTV3gvVlE7AL5E/G1jUr5Yg==} - '@volar/typescript@2.2.5': - resolution: {integrity: sha512-eSV/n75+ppfEVugMC/salZsI44nXDPAyL6+iTYCNLtiLHGJsnMv9GwiDMujrvAUj/aLQyqRJgYtXRoxop2clCw==} + '@volar/typescript@2.3.0-alpha.15': + resolution: {integrity: sha512-sTzUyqGC1wkzVyY0XZBO5smCPDRvIqxlMTUw7bZebuD/7sGaVfyk9ryE29aG6CLpuYDev/ugpQsKoFVhFaQQ8A==} '@vue/compiler-core@3.4.27': resolution: {integrity: sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==} @@ -2610,8 +2610,8 @@ packages: '@vue/compiler-ssr@3.4.27': resolution: {integrity: sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==} - '@vue/language-core@2.0.19': - resolution: {integrity: sha512-A9EGOnvb51jOvnCYoRLnMP+CcoPlbZVxI9gZXE/y2GksRWM6j/PrLEIC++pnosWTN08tFpJgxhSS//E9v/Sg+Q==} + '@vue/language-core@2.0.21': + resolution: {integrity: sha512-vjs6KwnCK++kIXT+eI63BGpJHfHNVJcUCr3RnvJsccT3vbJnZV5IhHR2puEkoOkIbDdp0Gqi1wEnv3hEd3WsxQ==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -7482,6 +7482,9 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vue-draggable-plus@0.5.0: resolution: {integrity: sha512-A5TT5+M5JceROSjPO9aDZTsrSN1TetEs419czPlboomarSiGIBIxTp2WD7XH53EHMrbO7Qo+leRiHWV/rMlyjA==} peerDependencies: @@ -7509,8 +7512,8 @@ packages: vue-template-compiler@2.7.16: resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==} - vue-tsc@2.0.19: - resolution: {integrity: sha512-JWay5Zt2/871iodGF72cELIbcAoPyhJxq56mPPh+M2K7IwI688FMrFKc/+DvB05wDWEuCPexQJ6L10zSwzzapg==} + vue-tsc@2.0.21: + resolution: {integrity: sha512-E6x1p1HaHES6Doy8pqtm7kQern79zRtIewkf9fiv7Y43Zo4AFDS5hKi+iHi2RwEhqRmuiwliB1LCEFEGwvxQnw==} hasBin: true peerDependencies: typescript: '*' @@ -9677,18 +9680,19 @@ snapshots: vite: 5.2.13(@types/node@20.14.2)(sass@1.77.4)(stylus@0.57.0)(terser@5.31.0) vue: 3.4.27(typescript@5.4.5) - '@volar/language-core@2.2.5': + '@volar/language-core@2.3.0-alpha.15': dependencies: - '@volar/source-map': 2.2.5 + '@volar/source-map': 2.3.0-alpha.15 - '@volar/source-map@2.2.5': + '@volar/source-map@2.3.0-alpha.15': dependencies: muggle-string: 0.4.1 - '@volar/typescript@2.2.5': + '@volar/typescript@2.3.0-alpha.15': dependencies: - '@volar/language-core': 2.2.5 + '@volar/language-core': 2.3.0-alpha.15 path-browserify: 1.0.1 + vscode-uri: 3.0.8 '@vue/compiler-core@3.4.27': dependencies: @@ -9728,9 +9732,9 @@ snapshots: '@vue/compiler-dom': 3.4.27 '@vue/shared': 3.4.27 - '@vue/language-core@2.0.19(typescript@5.4.5)': + '@vue/language-core@2.0.21(typescript@5.4.5)': dependencies: - '@volar/language-core': 2.2.5 + '@volar/language-core': 2.3.0-alpha.15 '@vue/compiler-dom': 3.4.27 '@vue/shared': 3.4.27 computeds: 0.0.1 @@ -15167,6 +15171,8 @@ snapshots: void-elements@3.1.0: {} + vscode-uri@3.0.8: {} + vue-draggable-plus@0.5.0(@types/sortablejs@1.15.8): dependencies: '@types/sortablejs': 1.15.8 @@ -15198,10 +15204,10 @@ snapshots: de-indent: 1.0.2 he: 1.2.0 - vue-tsc@2.0.19(typescript@5.4.5): + vue-tsc@2.0.21(typescript@5.4.5): dependencies: - '@volar/typescript': 2.2.5 - '@vue/language-core': 2.0.19(typescript@5.4.5) + '@volar/typescript': 2.3.0-alpha.15 + '@vue/language-core': 2.0.21(typescript@5.4.5) semver: 7.6.2 typescript: 5.4.5