diff --git a/crates/api/src/local_user/list_saved.rs b/crates/api/src/local_user/list_saved.rs index 5f0deff39..bdb1b6b0a 100644 --- a/crates/api/src/local_user/list_saved.rs +++ b/crates/api/src/local_user/list_saved.rs @@ -28,8 +28,10 @@ pub async fn list_person_saved( None }; let page_back = data.page_back; + let type_ = data.type_; let saved = PersonSavedCombinedQuery { + type_, page_after, page_back, } diff --git a/crates/api/src/local_user/verify_email.rs b/crates/api/src/local_user/verify_email.rs index 4b6a8c928..813b68364 100644 --- a/crates/api/src/local_user/verify_email.rs +++ b/crates/api/src/local_user/verify_email.rs @@ -19,6 +19,11 @@ pub async fn verify_email( let site_view = SiteView::read_local(&mut context.pool()).await?; let token = data.token.clone(); let verification = EmailVerification::read_for_token(&mut context.pool(), &token).await?; + let local_user_id = verification.local_user_id; + let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; + + // Check if their email has already been verified once, before this + let email_already_verified = local_user_view.local_user.email_verified; let form = LocalUserUpdateForm { // necessary in case this is a new signup @@ -27,18 +32,16 @@ pub async fn verify_email( email: Some(Some(verification.email)), ..Default::default() }; - let local_user_id = verification.local_user_id; LocalUser::update(&mut context.pool(), local_user_id, &form).await?; EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?; - // send out notification about registration application to admins if enabled - if site_view.local_site.application_email_admins { - let local_user = LocalUserView::read(&mut context.pool(), local_user_id).await?; - + // Send out notification about registration application to admins if enabled, and the user hasn't + // already been verified. + if site_view.local_site.application_email_admins && !email_already_verified { send_new_applicant_email_to_admins( - &local_user.person.name, + &local_user_view.person.name, &mut context.pool(), context.settings(), ) diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index 92a524543..4781683d4 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -4,6 +4,7 @@ use lemmy_db_schema::{ source::{login_token::LoginToken, site::Site}, CommentSortType, ListingType, + PersonContentType, PostListingMode, PostSortType, }; @@ -249,6 +250,8 @@ pub struct GetPersonDetailsResponse { /// /// Either person_id, or username are required. pub struct ListPersonContent { + #[cfg_attr(feature = "full", ts(optional))] + pub type_: Option, #[cfg_attr(feature = "full", ts(optional))] pub person_id: Option, /// Example: dessalines , or dessalines@xyz.tld @@ -275,6 +278,8 @@ pub struct ListPersonContentResponse { #[cfg_attr(feature = "full", ts(export))] /// Gets your saved posts and comments pub struct ListPersonSaved { + #[cfg_attr(feature = "full", ts(optional))] + pub type_: Option, #[cfg_attr(feature = "full", ts(optional))] pub page_cursor: Option, #[cfg_attr(feature = "full", ts(optional))] diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 590ff05eb..543e39495 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -97,6 +97,10 @@ pub struct GetPosts { #[cfg_attr(feature = "full", ts(optional))] pub community_name: Option, #[cfg_attr(feature = "full", ts(optional))] + pub saved_only: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub read_only: Option, + #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, diff --git a/crates/api_common/src/request.rs b/crates/api_common/src/request.rs index 02e889872..4ccd032b9 100644 --- a/crates/api_common/src/request.rs +++ b/crates/api_common/src/request.rs @@ -32,7 +32,7 @@ use reqwest::{ }; use reqwest_middleware::ClientWithMiddleware; use serde::{Deserialize, Serialize}; -use tracing::info; +use tracing::{info, warn}; use url::Url; use urlencoding::encode; use webpage::HTML; @@ -173,15 +173,23 @@ pub async fn generate_post_link_metadata( metadata.opengraph_data.image.clone() }; + // Attempt to generate a thumbnail depending on the instance settings. Either by proxying, + // storing image persistently in pict-rs or returning the remote url directly as thumbnail. let thumbnail_url = if let (false, Some(url)) = (is_image_post, custom_thumbnail) { - proxy_image_link(url, &context).await.ok() - } else if let (true, Some(url)) = (allow_generate_thumbnail, image_url) { + proxy_image_link(url.clone(), &context) + .await + .map_err(|e| warn!("Failed to proxy thumbnail: {e}")) + .ok() + .or(Some(url.into())) + } else if let (true, Some(url)) = (allow_generate_thumbnail, image_url.clone()) { generate_pictrs_thumbnail(&url, &context) .await + .map_err(|e| warn!("Failed to generate thumbnail: {e}")) .ok() .map(Into::into) + .or(image_url) } else { - metadata.opengraph_data.image.clone() + image_url.clone() }; let form = PostUpdateForm { diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index 2411b874a..45d241073 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -9,7 +9,7 @@ use actix_web::web::{Json, Query}; use lemmy_api_common::{ comment::{GetComments, GetCommentsResponse}, context::LemmyContext, - utils::check_private_instance, + utils::{check_conflicting_like_filters, check_private_instance}, }; use lemmy_db_schema::{ source::{comment::Comment, community::Community}, @@ -19,7 +19,7 @@ use lemmy_db_views::{ comment_view::CommentQuery, structs::{LocalUserView, SiteView}, }; -use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] pub async fn list_comments( @@ -49,9 +49,7 @@ pub async fn list_comments( let liked_only = data.liked_only; let disliked_only = data.disliked_only; - if liked_only.unwrap_or_default() && disliked_only.unwrap_or_default() { - return Err(LemmyError::from(LemmyErrorType::ContradictingFilters)); - } + check_conflicting_like_filters(liked_only, disliked_only)?; let page = data.page; let limit = data.limit; diff --git a/crates/apub/src/api/list_person_content.rs b/crates/apub/src/api/list_person_content.rs index 477e62e85..774f80766 100644 --- a/crates/apub/src/api/list_person_content.rs +++ b/crates/apub/src/api/list_person_content.rs @@ -37,9 +37,11 @@ pub async fn list_person_content( None }; let page_back = data.page_back; + let type_ = data.type_; let content = PersonContentCombinedQuery { creator_id: person_details_id, + type_, page_after, page_back, } diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index 6d043ae4f..7361c8f35 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -41,6 +41,7 @@ pub async fn list_posts( } else { data.community_id }; + let read_only = data.read_only; let show_hidden = data.show_hidden; let show_read = data.show_read; let show_nsfw = data.show_nsfw; @@ -76,6 +77,7 @@ pub async fn list_posts( listing_type, sort, community_id, + read_only, liked_only, disliked_only, page, diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index f4a81a751..7632db410 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -219,6 +219,16 @@ pub enum ModlogActionType { AdminAllowInstance, } +#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A list of possible types for the various modlog actions. +pub enum PersonContentType { + All, + Comments, + Posts, +} + #[derive( EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash, )] diff --git a/crates/db_views/src/person_content_combined_view.rs b/crates/db_views/src/person_content_combined_view.rs index e7a152219..edbb70093 100644 --- a/crates/db_views/src/person_content_combined_view.rs +++ b/crates/db_views/src/person_content_combined_view.rs @@ -43,6 +43,7 @@ use lemmy_db_schema::{ }, utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, InternalToCombinedView, + PersonContentType, }; use lemmy_utils::error::LemmyResult; @@ -82,6 +83,8 @@ pub struct PaginationCursorData(PersonContentCombined); pub struct PersonContentCombinedQuery { pub creator_id: PersonId, #[new(default)] + pub type_: Option, + #[new(default)] pub page_after: Option, #[new(default)] pub page_back: Option, @@ -208,6 +211,16 @@ impl PersonContentCombinedQuery { let mut query = PaginatedQueryBuilder::new(query); + if let Some(type_) = self.type_ { + query = match type_ { + PersonContentType::All => query, + PersonContentType::Comments => { + query.filter(person_content_combined::comment_id.is_not_null()) + } + PersonContentType::Posts => query.filter(person_content_combined::post_id.is_not_null()), + } + } + let page_after = self.page_after.map(|c| c.0); if self.page_back.unwrap_or_default() { diff --git a/crates/db_views/src/person_saved_combined_view.rs b/crates/db_views/src/person_saved_combined_view.rs index 27ed02386..e2aad01d4 100644 --- a/crates/db_views/src/person_saved_combined_view.rs +++ b/crates/db_views/src/person_saved_combined_view.rs @@ -40,6 +40,7 @@ use lemmy_db_schema::{ }, utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, InternalToCombinedView, + PersonContentType, }; use lemmy_utils::error::LemmyResult; @@ -77,6 +78,7 @@ pub struct PaginationCursorData(PersonSavedCombined); #[derive(Default)] pub struct PersonSavedCombinedQuery { + pub type_: Option, pub page_after: Option, pub page_back: Option, } @@ -210,6 +212,16 @@ impl PersonSavedCombinedQuery { let mut query = PaginatedQueryBuilder::new(query); + if let Some(type_) = self.type_ { + query = match type_ { + PersonContentType::All => query, + PersonContentType::Comments => { + query.filter(person_saved_combined::comment_id.is_not_null()) + } + PersonContentType::Posts => query.filter(person_saved_combined::post_id.is_not_null()), + } + } + let page_after = self.page_after.map(|c| c.0); if self.page_back.unwrap_or_default() { diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index a8f1259ad..b0a0fde67 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -306,6 +306,12 @@ fn queries<'a>() -> Queries< query = query.filter(post_aggregates::comments.eq(0)); }; + if o.read_only.unwrap_or_default() { + query = query + .filter(post_actions::read.is_not_null()) + .then_order_by(post_actions::read.desc()) + } + if !o.show_read.unwrap_or(o.local_user.show_read_posts()) { // Do not hide read posts when it is a user profile view // Or, only hide read posts on non-profile views @@ -496,6 +502,7 @@ pub struct PostQuery<'a> { pub local_user: Option<&'a LocalUser>, pub search_term: Option, pub url_only: Option, + pub read_only: Option, pub liked_only: Option, pub disliked_only: Option, pub title_only: Option, @@ -1192,6 +1199,34 @@ mod tests { Ok(()) } + #[test_context(Data)] + #[tokio::test] + #[serial] + async fn post_listing_read_only(data: &mut Data) -> LemmyResult<()> { + let pool = &data.pool(); + let pool = &mut pool.into(); + + // Only mark the bot post as read + // The read_only should only show the bot post + let post_read_form = + PostReadForm::new(data.inserted_bot_post.id, data.local_user_view.person.id); + PostRead::mark_as_read(pool, &post_read_form).await?; + + // Only read the post marked as read + let read_read_post_listing = PostQuery { + community_id: Some(data.inserted_community.id), + read_only: Some(true), + ..data.default_post_query() + } + .list(&data.site, pool) + .await?; + + // This should only include the bot post, not the one you created + assert_eq!(vec![POST_BY_BOT], names(&read_read_post_listing)); + + Ok(()) + } + #[test_context(Data)] #[tokio::test] #[serial] diff --git a/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/down.sql b/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/down.sql new file mode 100644 index 000000000..08750942c --- /dev/null +++ b/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/down.sql @@ -0,0 +1,2 @@ +DROP INDEX idx_post_actions_on_read_read_not_null; + diff --git a/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/up.sql b/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/up.sql new file mode 100644 index 000000000..03f9e4008 --- /dev/null +++ b/migrations/2024-12-15-151642_add_index_on_person_id_read_for_read_only_post_actions/up.sql @@ -0,0 +1,4 @@ +CREATE INDEX idx_post_actions_on_read_read_not_null ON post_actions (person_id, read, post_id) +WHERE + read IS NOT NULL; +