diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 9063008572..5f51bba686 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -419,19 +419,13 @@ export interface ImageSize { height: number } export function getImageSizeFromUrl(url: string): Promise -export interface PartialNoteToSummarize { - fileIds: Array - text: string | null - cw: string | null - hasPoll: boolean -} -export function getNoteSummary(note: PartialNoteToSummarize): string export function isQuote(note: Note): boolean export function isSafeUrl(url: string): boolean /** Returns the latest Firefish version. */ export function latestVersion(): Promise export function toMastodonId(firefishId: string): string | null export function fromMastodonId(mastodonId: string): string | null +export function getNoteSummary(fileIds: Array, text: string | undefined | null, cw: string | undefined | null, hasPoll: boolean): string /** * Converts the given text into the cat language. * diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index b4a6bea090..6559eef30f 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, fetchMeta, updateMetaCache, metaToPugArgs, loadConfig, stringToAcct, acctToString, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, updateNodeinfoCache, Protocol, Inbound, Outbound, greet, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isQuote, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrc, DriveFileUsageHint, MutedNoteReason, NoteVisibility, NotificationType, PageVisibility, PollNoteVisibility, RelayStatus, UserEmojiModPerm, UserProfileFfvisibility, UserProfileMutingNotificationTypes, updateAntennasOnNewNote, updateAntennaCache, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, ChatEvent, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding +const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, fetchMeta, updateMetaCache, metaToPugArgs, loadConfig, stringToAcct, acctToString, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, updateNodeinfoCache, Protocol, Inbound, Outbound, greet, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, isQuote, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, getNoteSummary, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrc, DriveFileUsageHint, MutedNoteReason, NoteVisibility, NotificationType, PageVisibility, PollNoteVisibility, RelayStatus, UserEmojiModPerm, UserProfileFfvisibility, UserProfileMutingNotificationTypes, updateAntennasOnNewNote, updateAntennaCache, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, ChatEvent, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding module.exports.SECOND = SECOND module.exports.MINUTE = MINUTE @@ -349,12 +349,12 @@ module.exports.sqlLikeEscape = sqlLikeEscape module.exports.safeForSql = safeForSql module.exports.formatMilliseconds = formatMilliseconds module.exports.getImageSizeFromUrl = getImageSizeFromUrl -module.exports.getNoteSummary = getNoteSummary module.exports.isQuote = isQuote module.exports.isSafeUrl = isSafeUrl module.exports.latestVersion = latestVersion module.exports.toMastodonId = toMastodonId module.exports.fromMastodonId = fromMastodonId +module.exports.getNoteSummary = getNoteSummary module.exports.nyaify = nyaify module.exports.hashPassword = hashPassword module.exports.verifyPassword = verifyPassword diff --git a/packages/backend-rs/src/misc/check_word_mute.rs b/packages/backend-rs/src/misc/check_word_mute.rs index 6faf9352ec..6c3e7875e6 100644 --- a/packages/backend-rs/src/misc/check_word_mute.rs +++ b/packages/backend-rs/src/misc/check_word_mute.rs @@ -1,4 +1,4 @@ -use crate::misc::get_note_all_texts::all_texts; +use crate::misc::note::elaborate; use once_cell::sync::Lazy; use regex::Regex; use sea_orm::DbErr; @@ -59,7 +59,7 @@ pub async fn check_word_mute( Ok(false) } else { Ok(check_word_mute_impl( - &all_texts!(note, true).await?, + &elaborate!(note, true).await?, muted_words, muted_patterns, )) diff --git a/packages/backend-rs/src/misc/get_note_summary.rs b/packages/backend-rs/src/misc/get_note_summary.rs deleted file mode 100644 index f2bfcd2470..0000000000 --- a/packages/backend-rs/src/misc/get_note_summary.rs +++ /dev/null @@ -1,93 +0,0 @@ -use serde::Deserialize; - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -#[crate::export(object)] -pub struct PartialNoteToSummarize { - pub file_ids: Vec, - pub text: Option, - pub cw: Option, - pub has_poll: bool, -} - -#[crate::export] -pub fn get_note_summary(note: PartialNoteToSummarize) -> String { - let mut buf: Vec = vec![]; - - if let Some(cw) = note.cw { - buf.push(cw) - } else if let Some(text) = note.text { - buf.push(text) - } - - match note.file_ids.len() { - 0 => (), - 1 => buf.push("📎".to_string()), - n => buf.push(format!("📎 ({})", n)), - }; - - if note.has_poll { - buf.push("📊".to_string()) - } - - buf.join(" ") -} - -#[cfg(test)] -mod unit_test { - use super::{get_note_summary, PartialNoteToSummarize}; - use pretty_assertions::assert_eq; - - #[test] - fn test_note_summary() { - let note = PartialNoteToSummarize { - file_ids: vec![], - text: Some("Hello world!".to_string()), - cw: None, - has_poll: false, - }; - assert_eq!(get_note_summary(note), "Hello world!"); - - let note_with_cw = PartialNoteToSummarize { - file_ids: vec![], - text: Some("Hello world!".to_string()), - cw: Some("Content warning".to_string()), - has_poll: false, - }; - assert_eq!(get_note_summary(note_with_cw), "Content warning"); - - let note_with_file_and_cw = PartialNoteToSummarize { - file_ids: vec!["9s7fmcqogiq4igin".to_string()], - text: None, - cw: Some("Selfie, no ec".to_string()), - has_poll: false, - }; - assert_eq!(get_note_summary(note_with_file_and_cw), "Selfie, no ec 📎"); - - let note_with_files_only = PartialNoteToSummarize { - file_ids: vec![ - "9s7fmcqogiq4igin".to_string(), - "9s7qrld5u14cey98".to_string(), - "9s7gebs5zgts4kca".to_string(), - "9s5z3e4vefqd29ee".to_string(), - ], - text: None, - cw: None, - has_poll: false, - }; - assert_eq!(get_note_summary(note_with_files_only), "📎 (4)"); - - let note_all = PartialNoteToSummarize { - file_ids: vec![ - "9s7fmcqogiq4igin".to_string(), - "9s7qrld5u14cey98".to_string(), - "9s7gebs5zgts4kca".to_string(), - "9s5z3e4vefqd29ee".to_string(), - ], - text: Some("Hello world!".to_string()), - cw: Some("Content warning".to_string()), - has_poll: true, - }; - assert_eq!(get_note_summary(note_all), "Content warning 📎 (4) 📊"); - } -} diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index 9d717ded7f..e3eb23a2eb 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -7,12 +7,11 @@ 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_quote; pub mod is_safe_url; pub mod latest_version; pub mod mastodon_id; +pub mod note; pub mod nyaify; pub mod password; pub mod reaction; diff --git a/packages/backend-rs/src/misc/get_note_all_texts.rs b/packages/backend-rs/src/misc/note/elaborate.rs similarity index 86% rename from packages/backend-rs/src/misc/get_note_all_texts.rs rename to packages/backend-rs/src/misc/note/elaborate.rs index 8db5fbc8f3..878dbb4f83 100644 --- a/packages/backend-rs/src/misc/get_note_all_texts.rs +++ b/packages/backend-rs/src/misc/note/elaborate.rs @@ -6,7 +6,7 @@ use sea_orm::{prelude::*, QuerySelect}; /// 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 +/// Consider using [`elaborate`] macro instead /// when dealing with a note ([`note::Model`])-like instance. /// /// # Arguments @@ -14,7 +14,7 @@ use sea_orm::{prelude::*, QuerySelect}; /// * `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_impl( +pub async fn elaborate_impl( file_ids: &[String], text: Option, cw: Option, @@ -87,15 +87,15 @@ pub async fn all_texts_impl( /// # Caveats /// /// The `note_like` argument should not contain function calls -/// (e.g., `all_texts!(note.clone(), false)`) +/// (e.g., `elaborate!(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; +/// # use backend_rs::misc::note::elaborate; /// // note-like struct -/// struct SomeNoteLikeStruct { +/// struct NoteLike { /// // required fields /// file_ids: Vec, /// text: Option, @@ -107,18 +107,19 @@ pub async fn all_texts_impl( /// 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 +/// async fn print_all_related_texts( +/// note: &NoteLike +/// ) -> Result<(), sea_orm::DbErr> { +/// let all_texts = elaborate!(note, true).await?; +/// all_texts.iter().map(|text| println!("{}", text)); +/// Ok(()) /// } /// ``` #[doc(hidden)] // hide the macro in the top doc page #[macro_export] -macro_rules! all_texts { +macro_rules! elaborate { ($note_like:expr, $include_parent:expr) => { - $crate::misc::get_note_all_texts::all_texts_impl( + $crate::misc::note::elaborate::elaborate_impl( &$note_like.file_ids, $note_like.text.clone(), $note_like.cw.clone(), @@ -128,5 +129,6 @@ macro_rules! all_texts { ) }; } + #[doc(inline)] // show the macro in the module doc page -pub use all_texts; +pub use elaborate; diff --git a/packages/backend-rs/src/misc/note/mod.rs b/packages/backend-rs/src/misc/note/mod.rs new file mode 100644 index 0000000000..84e2560af8 --- /dev/null +++ b/packages/backend-rs/src/misc/note/mod.rs @@ -0,0 +1,5 @@ +pub use elaborate::elaborate; +pub use summarize::summarize; + +pub mod elaborate; +pub mod summarize; diff --git a/packages/backend-rs/src/misc/note/summarize.rs b/packages/backend-rs/src/misc/note/summarize.rs new file mode 100644 index 0000000000..2bfabba71b --- /dev/null +++ b/packages/backend-rs/src/misc/note/summarize.rs @@ -0,0 +1,145 @@ +#[crate::export(js_name = "getNoteSummary")] +pub fn summarize_impl( + file_ids: &[String], + text: Option, + cw: Option, + has_poll: bool, +) -> String { + let mut buf: Vec = vec![]; + + if let Some(cw) = cw { + buf.push(cw) + } else if let Some(text) = text { + buf.push(text) + } + + match file_ids.len() { + 0 => (), + 1 => buf.push("📎".to_string()), + n => buf.push(format!("📎 ({})", n)), + }; + + if has_poll { + buf.push("📊".to_string()) + } + + buf.join(" ") +} + +/// Returns the summary of a post, which can be used to display posts in small spaces +/// such as push notifications. +/// +/// # Arguments +/// +/// * `note_like` : a note ([`crate::model::entity::note::Model`])-like instance containing +/// `file_ids`, `text`, `cw`, `has_poll` fields +/// +/// # Caveats +/// +/// The `note_like` argument should not contain function calls +/// (e.g., `summarize!(note.clone())`) +/// since the function will be called multiple times after macro expansion. +/// +/// # Examples +/// +/// ``` +/// # use backend_rs::misc::note::summarize; +/// // note-like struct +/// struct NoteLike { +/// // required fields +/// file_ids: Vec, +/// text: Option, +/// cw: Option, +/// has_poll: bool, +/// // arbitrary extra fields +/// renote_id: Option, +/// reply_id: Option, +/// extra_field_1: u32, +/// extra_field_2: Vec, +/// } +/// +/// fn print_note_summary(note: &NoteLike) { +/// println!("{}", summarize!(note)); +/// } +/// ``` +#[doc(hidden)] // hide the macro in the top doc page +#[macro_export] +macro_rules! summarize { + ($note_like:expr) => { + $crate::misc::note::summarize::summarize_impl( + &$note_like.file_ids, + $note_like.text.to_owned(), + $note_like.cw.to_owned(), + $note_like.has_poll.to_owned(), + ) + }; +} + +#[doc(inline)] // show the macro in the module doc page +pub use summarize; + +#[cfg(test)] +mod unit_test { + use super::summarize; + use pretty_assertions::assert_eq; + + struct NoteLike { + file_ids: Vec, + text: Option, + cw: Option, + has_poll: bool, + } + + #[test] + fn summarize() { + let note = NoteLike { + file_ids: vec![], + text: Some("Hello world!".to_string()), + cw: None, + has_poll: false, + }; + assert_eq!(summarize!(note), "Hello world!"); + + let note_with_cw = NoteLike { + file_ids: vec![], + text: Some("Hello world!".to_string()), + cw: Some("Content warning".to_string()), + has_poll: false, + }; + assert_eq!(summarize!(note_with_cw), "Content warning"); + + let note_with_file_and_cw = NoteLike { + file_ids: vec!["9s7fmcqogiq4igin".to_string()], + text: None, + cw: Some("Selfie, no ec".to_string()), + has_poll: false, + }; + assert_eq!(summarize!(note_with_file_and_cw), "Selfie, no ec 📎"); + + let note_with_files_only = NoteLike { + file_ids: vec![ + "9s7fmcqogiq4igin".to_string(), + "9s7qrld5u14cey98".to_string(), + "9s7gebs5zgts4kca".to_string(), + "9s5z3e4vefqd29ee".to_string(), + ], + text: None, + cw: None, + has_poll: false, + }; + assert_eq!(summarize!(note_with_files_only), "📎 (4)"); + + let note_all = NoteLike { + file_ids: vec![ + "9s7fmcqogiq4igin".to_string(), + "9s7qrld5u14cey98".to_string(), + "9s7gebs5zgts4kca".to_string(), + "9s5z3e4vefqd29ee".to_string(), + ], + text: Some("Hello world!".to_string()), + cw: Some("Content warning".to_string()), + has_poll: true, + }; + assert_eq!(summarize!(note_all), "Content warning 📎 (4) 📊"); + } +} 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 f4ec336681..a90ef11803 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, + misc::note::elaborate, model::entity::note, service::{ antenna, @@ -41,7 +41,7 @@ pub async fn update_antennas_on_new_note( note_author: &Acct, note_muted_users: &[String], ) -> Result<(), Error> { - let note_all_texts = all_texts!(note, false).await?; + let note_all_texts = elaborate!(note, false).await?; // TODO: do this in parallel for antenna in antenna::cache::get().await?.iter() { diff --git a/packages/backend-rs/src/service/push_notification.rs b/packages/backend-rs/src/service/push_notification.rs index 35c97f86c1..ccd83bf689 100644 --- a/packages/backend-rs/src/service/push_notification.rs +++ b/packages/backend-rs/src/service/push_notification.rs @@ -1,12 +1,10 @@ use crate::{ - config::local_server_info, - database::db_conn, - misc::get_note_summary::{get_note_summary, PartialNoteToSummarize}, - model::entity::sw_subscription, - util::http_client, + config::local_server_info, database::db_conn, misc::note::summarize, + model::entity::sw_subscription, util::http_client, }; use once_cell::sync::OnceCell; use sea_orm::prelude::*; +use serde::Deserialize; use web_push::*; #[derive(thiserror::Error, Debug)] @@ -72,8 +70,18 @@ fn compact_content(mut content: serde_json::Value) -> Result, + text: Option, + cw: Option, + has_poll: bool, + } + + let note_like: PartialNote = serde_json::from_value(note.clone())?; + let text = summarize!(note_like); let note_object = note.as_object_mut().unwrap(); diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index f776c19ed4..af6e237be6 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -540,7 +540,7 @@ router.get("/notes/:note", async (ctx, next) => { await Users.findOneByOrFail({ id: note.userId }), ), // TODO: Let locale changeable by instance setting - summary: getNoteSummary(note), + summary: getNoteSummary(note.fileIds, note.text, note.cw, note.hasPoll), }); ctx.set("Cache-Control", "public, max-age=15"); @@ -562,19 +562,19 @@ router.get("/posts/:note", async (ctx, next) => { visibility: In(["public", "home"]), }); - if (note) { - const _note = await Notes.pack(note); + if (note != null) { + const packedNote = await Notes.pack(note); const profile = await UserProfiles.findOneByOrFail({ userId: note.userId }); const meta = await fetchMeta(); await ctx.render("note", { ...metaToPugArgs(meta), - note: _note, + note: packedNote, profile, avatarUrl: await Users.getAvatarUrl( await Users.findOneByOrFail({ id: note.userId }), ), // TODO: Let locale changeable by instance setting - summary: getNoteSummary(_note), + summary: getNoteSummary(note.fileIds, note.text, note.cw, note.hasPoll), }); ctx.set("Cache-Control", "public, max-age=15");