From 8a6a86c1bb53a27dcde9f3675946993a59ba14c5 Mon Sep 17 00:00:00 2001 From: Lcchy Date: Tue, 13 Feb 2024 09:46:46 +0000 Subject: [PATCH 01/12] Add support for RSS media enclosures in feeds (#4442) * Add support for RSS media enclosures in feeds * Use post.url_content_type --- crates/routes/src/feeds.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 9695401df..490fce4ae 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -29,6 +29,7 @@ use once_cell::sync::Lazy; use rss::{ extension::{dublincore::DublinCoreExtension, ExtensionBuilder, ExtensionMap}, Channel, + EnclosureBuilder, Guid, Item, }; @@ -495,7 +496,6 @@ fn create_post_items( let mut items: Vec = Vec::new(); for p in posts { - // TODO add images let post_url = format!("{}/post/{}", protocol_and_hostname, p.post.id); let community_url = format!( "{}/c/{}", @@ -520,12 +520,21 @@ fn create_post_items( p.counts.comments); // If its a url post, add it to the description - let link = Some(if let Some(url) = p.post.url { + // and see if we can parse it as a media enclosure. + let enclosure_opt = p.post.url.map(|url| { let link_html = format!("
{url}"); description.push_str(&link_html); - url.to_string() - } else { - post_url.clone() + + let mime_type = p + .post + .url_content_type + .unwrap_or_else(|| "application/octet-stream".to_string()); + let mut enclosure_bld = EnclosureBuilder::default(); + + enclosure_bld.url(url.as_str().to_string()); + enclosure_bld.mime_type(mime_type); + enclosure_bld.length("0".to_string()); + enclosure_bld.build() }); if let Some(body) = p.post.body { @@ -558,8 +567,9 @@ fn create_post_items( guid, description: Some(sanitize_xml(description)), dublin_core_ext, - link, + link: Some(post_url.clone()), extensions, + enclosure: enclosure_opt, ..Default::default() }; From 33989f5518331545be8f0a758e8d70624f9144bb Mon Sep 17 00:00:00 2001 From: Dessalines Date: Wed, 14 Feb 2024 04:49:55 -0500 Subject: [PATCH 02/12] Blocking an instance also hides private messages from their users. (#4447) * Blocking an instance also hides private messages from their users. - Fixes #4444 * Separating private message tests. --- crates/db_schema/src/impls/local_user.rs | 9 ++ crates/db_schema/src/impls/person.rs | 12 +- crates/db_views/src/post_view.rs | 32 ++--- crates/db_views/src/private_message_view.rs | 132 ++++++++++++++++++-- 4 files changed, 152 insertions(+), 33 deletions(-) diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 98d1c8494..58c37ff3c 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -140,6 +140,15 @@ impl LocalUser { } } +impl LocalUserInsertForm { + pub fn test_form(person_id: PersonId) -> Self { + Self::builder() + .person_id(person_id) + .password_encrypted(String::new()) + .build() + } +} + pub struct UserBackupLists { pub followed_communities: Vec, pub saved_posts: Vec, diff --git a/crates/db_schema/src/impls/person.rs b/crates/db_schema/src/impls/person.rs index 32ce6c97a..9fb1ee1c5 100644 --- a/crates/db_schema/src/impls/person.rs +++ b/crates/db_schema/src/impls/person.rs @@ -1,5 +1,5 @@ use crate::{ - newtypes::{CommunityId, DbUrl, PersonId}, + newtypes::{CommunityId, DbUrl, InstanceId, PersonId}, schema::{instance, local_user, person, person_follower}, source::person::{ Person, @@ -86,6 +86,16 @@ impl Person { } } +impl PersonInsertForm { + pub fn test_form(instance_id: InstanceId, name: &str) -> Self { + Self::builder() + .name(name.to_owned()) + .public_key("pubkey".to_string()) + .instance_id(instance_id) + .build() + } +} + #[async_trait] impl ApubActor for Person { async fn read_from_apub_id( diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index cdfa923d4..6c5a983b1 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -698,7 +698,7 @@ mod tests { use lemmy_db_schema::{ aggregates::structs::PostAggregates, impls::actor_language::UNDETERMINED_ID, - newtypes::{InstanceId, LanguageId, PersonId}, + newtypes::LanguageId, source::{ actor_language::LocalUserLanguage, comment::{Comment, CommentInsertForm}, @@ -757,37 +757,22 @@ mod tests { } } - fn default_person_insert_form(instance_id: InstanceId, name: &str) -> PersonInsertForm { - PersonInsertForm::builder() - .name(name.to_owned()) - .public_key("pubkey".to_string()) - .instance_id(instance_id) - .build() - } - - fn default_local_user_form(person_id: PersonId) -> LocalUserInsertForm { - LocalUserInsertForm::builder() - .person_id(person_id) - .password_encrypted(String::new()) - .build() - } - async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - let new_person = default_person_insert_form(inserted_instance.id, "tegan"); + let new_person = PersonInsertForm::test_form(inserted_instance.id, "tegan"); let inserted_person = Person::create(pool, &new_person).await?; let local_user_form = LocalUserInsertForm { admin: Some(true), - ..default_local_user_form(inserted_person.id) + ..LocalUserInsertForm::test_form(inserted_person.id) }; let inserted_local_user = LocalUser::create(pool, &local_user_form).await?; let new_bot = PersonInsertForm { bot_account: Some(true), - ..default_person_insert_form(inserted_instance.id, "mybot") + ..PersonInsertForm::test_form(inserted_instance.id, "mybot") }; let inserted_bot = Person::create(pool, &new_bot).await?; @@ -802,12 +787,15 @@ mod tests { let inserted_community = Community::create(pool, &new_community).await?; // Test a person block, make sure the post query doesn't include their post - let blocked_person = default_person_insert_form(inserted_instance.id, "john"); + let blocked_person = PersonInsertForm::test_form(inserted_instance.id, "john"); let inserted_blocked_person = Person::create(pool, &blocked_person).await?; - let inserted_blocked_local_user = - LocalUser::create(pool, &default_local_user_form(inserted_blocked_person.id)).await?; + let inserted_blocked_local_user = LocalUser::create( + pool, + &LocalUserInsertForm::test_form(inserted_blocked_person.id), + ) + .await?; let post_from_blocked_person = PostInsertForm::builder() .name(POST_BY_BLOCKED_PERSON.to_string()) diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs index d04ff7b49..a3e2469c9 100644 --- a/crates/db_views/src/private_message_view.rs +++ b/crates/db_views/src/private_message_view.rs @@ -12,7 +12,7 @@ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases, newtypes::{PersonId, PrivateMessageId}, - schema::{person, person_block, private_message}, + schema::{instance_block, person, person_block, private_message}, utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, }; use tracing::debug; @@ -34,6 +34,13 @@ fn queries<'a>() -> Queries< .and(person_block::person_id.eq(aliases::person1.field(person::id))), ), ) + .left_join( + instance_block::table.on( + person::instance_id + .eq(instance_block::instance_id) + .and(instance_block::person_id.eq(aliases::person1.field(person::id))), + ), + ) }; let selection = ( @@ -55,7 +62,9 @@ fn queries<'a>() -> Queries< let mut query = all_joins(private_message::table.into_boxed()) .select(selection) // Dont show replies from blocked users - .filter(person_block::person_id.is_null()); + .filter(person_block::person_id.is_null()) + // Dont show replies from blocked instances + .filter(instance_block::person_id.is_null()); // If its unread, I only want the ones to me if options.unread_only { @@ -116,6 +125,8 @@ impl PrivateMessageView { use diesel::dsl::count; let conn = &mut get_conn(pool).await?; private_message::table + // Necessary to get the senders instance_id + .inner_join(person::table.on(private_message::creator_id.eq(person::id))) .left_join( person_block::table.on( private_message::creator_id @@ -123,8 +134,17 @@ impl PrivateMessageView { .and(person_block::person_id.eq(my_person_id)), ), ) + .left_join( + instance_block::table.on( + person::instance_id + .eq(instance_block::instance_id) + .and(instance_block::person_id.eq(my_person_id)), + ), + ) // Dont count replies from blocked users .filter(person_block::person_id.is_null()) + // Dont count replies from blocked instances + .filter(instance_block::person_id.is_null()) .filter(private_message::read.eq(false)) .filter(private_message::recipient_id.eq(my_person_id)) .filter(private_message::deleted.eq(false)) @@ -160,24 +180,30 @@ mod tests { use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView}; use lemmy_db_schema::{ assert_length, + newtypes::InstanceId, source::{ instance::Instance, + instance_block::{InstanceBlock, InstanceBlockForm}, person::{Person, PersonInsertForm}, person_block::{PersonBlock, PersonBlockForm}, private_message::{PrivateMessage, PrivateMessageInsertForm}, }, traits::{Blockable, Crud}, - utils::build_db_pool_for_tests, + utils::{build_db_pool_for_tests, DbPool}, }; + use lemmy_utils::error::LemmyResult; use pretty_assertions::assert_eq; use serial_test::serial; - #[tokio::test] - #[serial] - async fn test_crud() { + struct Data { + instance: Instance, + timmy: Person, + jess: Person, + sara: Person, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { let message_content = String::new(); - let pool = &build_db_pool_for_tests().await; - let pool = &mut pool.into(); let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()) .await @@ -243,6 +269,32 @@ mod tests { .await .unwrap(); + Ok(Data { + instance, + timmy, + jess, + sara, + }) + } + + async fn cleanup(instance_id: InstanceId, pool: &mut DbPool<'_>) -> LemmyResult<()> { + // This also deletes all persons and private messages thanks to sql `on delete cascade` + Instance::delete(pool, instance_id).await.unwrap(); + Ok(()) + } + + #[tokio::test] + #[serial] + async fn read_private_messages() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + let Data { + timmy, + jess, + sara, + instance, + } = init_data(pool).await?; + let timmy_messages = PrivateMessageQuery { unread_only: false, creator_id: None, @@ -303,6 +355,21 @@ mod tests { assert_eq!(timmy_sara_unread_messages[0].creator.id, sara.id); assert_eq!(timmy_sara_unread_messages[0].recipient.id, timmy.id); + cleanup(instance.id, pool).await + } + + #[tokio::test] + #[serial] + async fn ensure_person_block() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + let Data { + timmy, + sara, + instance, + jess: _, + } = init_data(pool).await?; + // Make sure blocks are working let timmy_blocks_sara_form = PersonBlockForm { person_id: timmy.id, @@ -336,7 +403,52 @@ mod tests { .unwrap(); assert_eq!(timmy_unread_messages, 1); - // This also deletes all persons and private messages thanks to sql `on delete cascade` - Instance::delete(pool, instance.id).await.unwrap(); + cleanup(instance.id, pool).await + } + + #[tokio::test] + #[serial] + async fn ensure_instance_block() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + let Data { + timmy, + jess: _, + sara, + instance, + } = init_data(pool).await?; + // Make sure instance_blocks are working + let timmy_blocks_instance_form = InstanceBlockForm { + person_id: timmy.id, + instance_id: sara.instance_id, + }; + + let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form) + .await + .unwrap(); + + let expected_instance_block = InstanceBlock { + person_id: timmy.id, + instance_id: sara.instance_id, + published: inserted_instance_block.published, + }; + assert_eq!(expected_instance_block, inserted_instance_block); + + let timmy_messages = PrivateMessageQuery { + unread_only: true, + creator_id: None, + ..Default::default() + } + .list(pool, timmy.id) + .await + .unwrap(); + + assert_length!(0, &timmy_messages); + + let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id) + .await + .unwrap(); + assert_eq!(timmy_unread_messages, 0); + cleanup(instance.id, pool).await } } From 3f7cc07b024ea23f4ad277a8515f5f92d1e1c0c2 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 15 Feb 2024 04:42:23 -0500 Subject: [PATCH 03/12] Adding ability to specify a custom post thumbnail. (#4425) * Adding ability to specify a custom post thumbnail. - Context: #4204 * Fixing ts-rs serialization. * Fixing conversion, adding checks. * Proxying custom_thumbnail. Fixed logic for update. * Only generate metadata thumbnail is theres no custom thumbnail. --- crates/api_common/src/post.rs | 6 ++++++ crates/api_crud/src/post/create.rs | 16 +++++++++++++--- crates/api_crud/src/post/update.rs | 24 ++++++++++++++++++++---- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 36f971ca7..1db07e451 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -28,6 +28,9 @@ pub struct CreatePost { pub honeypot: Option, pub nsfw: Option, pub language_id: Option, + #[cfg_attr(feature = "full", ts(type = "string"))] + /// Instead of fetching a thumbnail, use a custom one. + pub custom_thumbnail: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -114,6 +117,9 @@ pub struct EditPost { pub body: Option, pub nsfw: Option, pub language_id: Option, + #[cfg_attr(feature = "full", ts(type = "string"))] + /// Instead of fetching a thumbnail, use a custom one. + pub custom_thumbnail: Option, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 910454d5b..a5a9c013f 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -57,10 +57,12 @@ pub async fn create_post( let data_url = data.url.as_ref(); let url = data_url.map(clean_url_params); // TODO no good way to handle a "clear" + let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params); is_valid_post_title(&data.name)?; is_valid_body_field(&body, true)?; - check_url_scheme(&data.url)?; + check_url_scheme(&url)?; + check_url_scheme(&custom_thumbnail)?; check_community_user_action( &local_user_view.person, @@ -84,9 +86,17 @@ pub async fn create_post( } } + // Only generate the thumbnail if there's no custom thumbnail provided, + // otherwise it will save it in pictrs + let generate_thumbnail = custom_thumbnail.is_none(); + // Fetch post links and pictrs cached image - let metadata = fetch_link_metadata_opt(url.as_ref(), true, &context).await; + let metadata = fetch_link_metadata_opt(url.as_ref(), generate_thumbnail, &context).await; let url = proxy_image_link_opt_apub(url, &context).await?; + let thumbnail_url = proxy_image_link_opt_apub(custom_thumbnail, &context) + .await? + .map(Into::into) + .or(metadata.thumbnail); // Only need to check if language is allowed in case user set it explicitly. When using default // language, it already only returns allowed languages. @@ -121,7 +131,7 @@ pub async fn create_post( .embed_description(metadata.opengraph_data.description) .embed_video_url(metadata.opengraph_data.embed_video_url) .language_id(language_id) - .thumbnail_url(metadata.thumbnail) + .thumbnail_url(thumbnail_url) .build(); let inserted_post = Post::create(&mut context.pool(), &post_form) diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index c36718615..e858d9b30 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -43,6 +43,7 @@ pub async fn update_post( // TODO No good way to handle a clear. // Issue link: https://github.com/LemmyNet/lemmy/issues/2287 let url = data.url.as_ref().map(clean_url_params); + let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params); let slur_regex = local_site_to_slur_regex(&local_site); check_slurs_opt(&data.name, &slur_regex)?; @@ -53,7 +54,8 @@ pub async fn update_post( } is_valid_body_field(&body, true)?; - check_url_scheme(&data.url)?; + check_url_scheme(&url)?; + check_url_scheme(&custom_thumbnail)?; let post_id = data.post_id; let orig_post = Post::read(&mut context.pool(), post_id).await?; @@ -70,10 +72,14 @@ pub async fn update_post( Err(LemmyErrorType::NoPostEditAllowed)? } - // Fetch post links and Pictrs cached image if url was updated - let (embed_title, embed_description, embed_video_url, thumbnail_url) = match &url { + // Fetch post links and thumbnail if url was updated + let (embed_title, embed_description, embed_video_url, metadata_thumbnail) = match &url { Some(url) => { - let metadata = fetch_link_metadata(url, true, &context).await?; + // Only generate the thumbnail if there's no custom thumbnail provided, + // otherwise it will save it in pictrs + let generate_thumbnail = custom_thumbnail.is_none(); + + let metadata = fetch_link_metadata(url, generate_thumbnail, &context).await?; ( Some(metadata.opengraph_data.title), Some(metadata.opengraph_data.description), @@ -83,11 +89,21 @@ pub async fn update_post( } _ => Default::default(), }; + let url = match url { Some(url) => Some(proxy_image_link_opt_apub(Some(url), &context).await?), _ => Default::default(), }; + let custom_thumbnail = match custom_thumbnail { + Some(custom_thumbnail) => { + Some(proxy_image_link_opt_apub(Some(custom_thumbnail), &context).await?) + } + _ => Default::default(), + }; + + let thumbnail_url = custom_thumbnail.or(metadata_thumbnail); + let language_id = data.language_id; CommunityLanguage::is_allowed_community_language( &mut context.pool(), From 890565ca14214413af91d63c4f91d8a7416ff881 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 15 Feb 2024 07:50:53 -0500 Subject: [PATCH 04/12] Deleting denied local_users older than a week. Fixes #4434 (#4448) * Deleting denied local_users older than a week. Fixes #4434 * Addressing PR comments. * Upping rust to 1.76 * Delete the person rows also. --- .woodpecker.yml | 2 +- crates/db_schema/src/impls/local_user.rs | 67 ++++++++++++++++-------- docker/Dockerfile | 2 +- src/scheduled_tasks.rs | 59 ++++++++++++--------- 4 files changed, 82 insertions(+), 48 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index a51a1499d..4bac67169 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -2,7 +2,7 @@ # See https://github.com/woodpecker-ci/woodpecker/issues/1677 variables: - - &rust_image "rust:1.75" + - &rust_image "rust:1.76" - &install_pnpm "corepack enable pnpm" - &slow_check_paths - path: diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs index 58c37ff3c..14da24bae 100644 --- a/crates/db_schema/src/impls/local_user.rs +++ b/crates/db_schema/src/impls/local_user.rs @@ -1,12 +1,6 @@ use crate::{ newtypes::{DbUrl, LocalUserId, PersonId}, - schema::local_user::dsl::{ - accepted_application, - email, - email_verified, - local_user, - password_encrypted, - }, + schema::{local_user, person, registration_application}, source::{ actor_language::{LocalUserLanguage, SiteLanguage}, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, @@ -15,11 +9,18 @@ use crate::{ utils::{ functions::{coalesce, lower}, get_conn, + now, DbPool, }, }; use bcrypt::{hash, DEFAULT_COST}; -use diesel::{dsl::insert_into, result::Error, ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel::{ + dsl::{insert_into, not, IntervalDsl}, + result::Error, + ExpressionMethods, + JoinOnDsl, + QueryDsl, +}; use diesel_async::RunQueryDsl; impl LocalUser { @@ -31,16 +32,16 @@ impl LocalUser { let conn = &mut get_conn(pool).await?; let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password"); - diesel::update(local_user.find(local_user_id)) - .set((password_encrypted.eq(password_hash),)) + diesel::update(local_user::table.find(local_user_id)) + .set((local_user::password_encrypted.eq(password_hash),)) .get_result::(conn) .await } pub async fn set_all_users_email_verified(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; - diesel::update(local_user) - .set(email_verified.eq(true)) + diesel::update(local_user::table) + .set(local_user::email_verified.eq(true)) .get_results::(conn) .await } @@ -49,18 +50,43 @@ impl LocalUser { pool: &mut DbPool<'_>, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - diesel::update(local_user) - .set(accepted_application.eq(true)) + diesel::update(local_user::table) + .set(local_user::accepted_application.eq(true)) .get_results::(conn) .await } - pub async fn is_email_taken(pool: &mut DbPool<'_>, email_: &str) -> Result { + pub async fn delete_old_denied_local_users(pool: &mut DbPool<'_>) -> Result { + let conn = &mut get_conn(pool).await?; + + // Make sure: + // - The deny reason exists + // - The app is older than a week + // - The accepted_application is false + let old_denied_registrations = registration_application::table + .filter(registration_application::deny_reason.is_not_null()) + .filter(registration_application::published.lt(now() - 1.week())) + .select(registration_application::local_user_id); + + // Delete based on join logic is here: + // https://stackoverflow.com/questions/60836040/how-do-i-perform-a-delete-with-sub-query-in-diesel-against-a-postgres-database + let local_users = local_user::table + .filter(local_user::id.eq_any(old_denied_registrations)) + .filter(not(local_user::accepted_application)) + .select(local_user::person_id); + + // Delete the person rows, which should automatically clear the local_user ones + let persons = person::table.filter(person::id.eq_any(local_users)); + + diesel::delete(persons).execute(conn).await + } + + pub async fn is_email_taken(pool: &mut DbPool<'_>, email: &str) -> Result { use diesel::dsl::{exists, select}; let conn = &mut get_conn(pool).await?; - select(exists( - local_user.filter(lower(coalesce(email, "")).eq(email_.to_lowercase())), - )) + select(exists(local_user::table.filter( + lower(coalesce(local_user::email, "")).eq(email.to_lowercase()), + ))) .get_result(conn) .await } @@ -78,7 +104,6 @@ impl LocalUser { community_follower, instance, instance_block, - person, person_block, post, post_saved, @@ -171,7 +196,7 @@ impl Crud for LocalUser { hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password"); form_with_encrypted_password.password_encrypted = password_hash; - let local_user_ = insert_into(local_user) + let local_user_ = insert_into(local_user::table) .values(form_with_encrypted_password) .get_result::(conn) .await?; @@ -194,7 +219,7 @@ impl Crud for LocalUser { form: &Self::UpdateForm, ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::update(local_user.find(local_user_id)) + diesel::update(local_user::table.find(local_user_id)) .set(form) .get_result::(conn) .await diff --git a/docker/Dockerfile b/docker/Dockerfile index e33b99cfb..1bbf4ddbd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.6 -ARG RUST_VERSION=1.75 +ARG RUST_VERSION=1.76 ARG CARGO_BUILD_FEATURES=default ARG RUST_RELEASE_MODE=debug diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 7433c9638..38eb4ece2 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -22,7 +22,10 @@ use lemmy_db_schema::{ received_activity, sent_activity, }, - source::instance::{Instance, InstanceForm}, + source::{ + instance::{Instance, InstanceForm}, + local_user::LocalUser, + }, utils::{get_conn, naive_now, now, DbPool, DELETED_REPLACEMENT_TEXT}, }; use lemmy_routes::nodeinfo::NodeInfo; @@ -79,24 +82,19 @@ pub async fn setup(context: LemmyContext) -> Result<(), LemmyError> { }); let context_1 = context.clone(); - // Overwrite deleted & removed posts and comments every day + // Daily tasks: + // - Overwrite deleted & removed posts and comments every day + // - Delete old denied users + // - Update instance software scheduler.every(CTimeUnits::days(1)).run(move || { let context = context_1.clone(); async move { overwrite_deleted_posts_and_comments(&mut context.pool()).await; - } - }); - - let context_1 = context.clone(); - // Update the Instance Software - scheduler.every(CTimeUnits::days(1)).run(move || { - let context = context_1.clone(); - - async move { + delete_old_denied_users(&mut context.pool()).await; update_instance_software(&mut context.pool(), context.client()) .await - .map_err(|e| warn!("Failed to update instance software: {e}")) + .inspect_err(|e| warn!("Failed to update instance software: {e}")) .ok(); } }); @@ -115,6 +113,7 @@ async fn startup_jobs(pool: &mut DbPool<'_>) { update_banned_when_expired(pool).await; clear_old_activities(pool).await; overwrite_deleted_posts_and_comments(pool).await; + delete_old_denied_users(pool).await; } /// Update the hot_rank columns for the aggregates tables @@ -277,10 +276,10 @@ async fn delete_expired_captcha_answers(pool: &mut DbPool<'_>) { ) .execute(&mut conn) .await - .map(|_| { + .inspect(|_| { info!("Done."); }) - .map_err(|e| error!("Failed to clear old captcha answers: {e}")) + .inspect_err(|e| error!("Failed to clear old captcha answers: {e}")) .ok(); } Err(e) => { @@ -301,7 +300,7 @@ async fn clear_old_activities(pool: &mut DbPool<'_>) { ) .execute(&mut conn) .await - .map_err(|e| error!("Failed to clear old sent activities: {e}")) + .inspect_err(|e| error!("Failed to clear old sent activities: {e}")) .ok(); diesel::delete( @@ -310,8 +309,8 @@ async fn clear_old_activities(pool: &mut DbPool<'_>) { ) .execute(&mut conn) .await - .map(|_| info!("Done.")) - .map_err(|e| error!("Failed to clear old received activities: {e}")) + .inspect(|_| info!("Done.")) + .inspect_err(|e| error!("Failed to clear old received activities: {e}")) .ok(); } Err(e) => { @@ -320,6 +319,16 @@ async fn clear_old_activities(pool: &mut DbPool<'_>) { } } +async fn delete_old_denied_users(pool: &mut DbPool<'_>) { + LocalUser::delete_old_denied_local_users(pool) + .await + .inspect(|_| { + info!("Done."); + }) + .inspect(|e| error!("Failed to deleted old denied users: {e}")) + .ok(); +} + /// overwrite posts and comments 30d after deletion async fn overwrite_deleted_posts_and_comments(pool: &mut DbPool<'_>) { info!("Overwriting deleted posts..."); @@ -339,10 +348,10 @@ async fn overwrite_deleted_posts_and_comments(pool: &mut DbPool<'_>) { )) .execute(&mut conn) .await - .map(|_| { + .inspect(|_| { info!("Done."); }) - .map_err(|e| error!("Failed to overwrite deleted posts: {e}")) + .inspect_err(|e| error!("Failed to overwrite deleted posts: {e}")) .ok(); info!("Overwriting deleted comments..."); @@ -355,10 +364,10 @@ async fn overwrite_deleted_posts_and_comments(pool: &mut DbPool<'_>) { .set(comment::content.eq(DELETED_REPLACEMENT_TEXT)) .execute(&mut conn) .await - .map(|_| { + .inspect(|_| { info!("Done."); }) - .map_err(|e| error!("Failed to overwrite deleted comments: {e}")) + .inspect_err(|e| error!("Failed to overwrite deleted comments: {e}")) .ok(); } Err(e) => { @@ -390,14 +399,14 @@ async fn active_counts(pool: &mut DbPool<'_>) { sql_query(update_site_stmt) .execute(&mut conn) .await - .map_err(|e| error!("Failed to update site stats: {e}")) + .inspect_err(|e| error!("Failed to update site stats: {e}")) .ok(); let update_community_stmt = format!("update community_aggregates ca set users_active_{} = mv.count_ from community_aggregates_activity('{}') mv where ca.community_id = mv.community_id_", i.1, i.0); sql_query(update_community_stmt) .execute(&mut conn) .await - .map_err(|e| error!("Failed to update community stats: {e}")) + .inspect_err(|e| error!("Failed to update community stats: {e}")) .ok(); } @@ -424,7 +433,7 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) { .set(person::banned.eq(false)) .execute(&mut conn) .await - .map_err(|e| error!("Failed to update person.banned when expires: {e}")) + .inspect_err(|e| error!("Failed to update person.banned when expires: {e}")) .ok(); diesel::delete( @@ -432,7 +441,7 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) { ) .execute(&mut conn) .await - .map_err(|e| error!("Failed to remove community_ban expired rows: {e}")) + .inspect_err(|e| error!("Failed to remove community_ban expired rows: {e}")) .ok(); } Err(e) => { From a3bf2f1cf12c30a6df299105735ff3ebf90b4f5b Mon Sep 17 00:00:00 2001 From: Dessalines Date: Thu, 15 Feb 2024 08:52:04 -0500 Subject: [PATCH 05/12] Auto resolve reports on removing a comment or post. Fixes #4390 (#4402) * Automatically resolve report when post/comment is removed (#3850) * Automatically resolve report when post/comment is removed * also handle apub removes * Removing auto-resolve report triggers. * Dont allow creating reports for deleted / removed items. * Running pgformat. * Fixing test. * Addressing PR comments. * Forgot comment report. --------- Co-authored-by: Nutomic --- crates/api/src/comment_report/create.rs | 9 +++- crates/api/src/post_report/create.rs | 8 +++- crates/api_common/src/utils.rs | 9 ++++ crates/api_crud/src/comment/remove.rs | 6 ++- crates/api_crud/src/post/remove.rs | 6 ++- .../apub/src/activities/community/report.rs | 9 +++- crates/apub/src/activities/deletion/delete.rs | 6 ++- crates/db_schema/src/impls/comment_report.rs | 24 +++++++++- crates/db_schema/src/impls/post_report.rs | 42 ++++++++++++++-- .../src/impls/private_message_report.rs | 12 ++++- crates/db_schema/src/traits.rs | 8 ++++ crates/db_views/src/post_report_view.rs | 14 ++---- .../down.sql | 48 +++++++++++++++++++ .../up.sql | 8 ++++ 14 files changed, 188 insertions(+), 21 deletions(-) create mode 100644 migrations/2024-01-25-151400_remove_auto_resolve_report_trigger/down.sql create mode 100644 migrations/2024-01-25-151400_remove_auto_resolve_report_trigger/up.sql diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/comment_report/create.rs index 51f972b57..f8075460f 100644 --- a/crates/api/src/comment_report/create.rs +++ b/crates/api/src/comment_report/create.rs @@ -5,7 +5,11 @@ use lemmy_api_common::{ comment::{CommentReportResponse, CreateCommentReport}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_community_user_action, send_new_report_email_to_admins}, + utils::{ + check_comment_deleted_or_removed, + check_community_user_action, + send_new_report_email_to_admins, + }, }; use lemmy_db_schema::{ source::{ @@ -40,6 +44,9 @@ pub async fn create_comment_report( ) .await?; + // Don't allow creating reports for removed / deleted comments + check_comment_deleted_or_removed(&comment_view.comment)?; + let report_form = CommentReportForm { creator_id: person_id, comment_id, diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/post_report/create.rs index 0840cfee4..1327d7bb9 100644 --- a/crates/api/src/post_report/create.rs +++ b/crates/api/src/post_report/create.rs @@ -5,7 +5,11 @@ use lemmy_api_common::{ context::LemmyContext, post::{CreatePostReport, PostReportResponse}, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_community_user_action, send_new_report_email_to_admins}, + utils::{ + check_community_user_action, + check_post_deleted_or_removed, + send_new_report_email_to_admins, + }, }; use lemmy_db_schema::{ source::{ @@ -40,6 +44,8 @@ pub async fn create_post_report( ) .await?; + check_post_deleted_or_removed(&post_view.post)?; + let report_form = PostReportForm { creator_id: person_id, post_id, diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 55df7a6ec..138364c84 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -225,6 +225,7 @@ pub async fn check_community_mod_action( Ok(()) } +/// Don't allow creating reports for removed / deleted posts pub fn check_post_deleted_or_removed(post: &Post) -> Result<(), LemmyError> { if post.deleted || post.removed { Err(LemmyErrorType::Deleted)? @@ -233,6 +234,14 @@ pub fn check_post_deleted_or_removed(post: &Post) -> Result<(), LemmyError> { } } +pub fn check_comment_deleted_or_removed(comment: &Comment) -> Result<(), LemmyError> { + if comment.deleted || comment.removed { + Err(LemmyErrorType::Deleted)? + } else { + Ok(()) + } +} + /// Throws an error if a recipient has blocked a person. #[tracing::instrument(skip_all)] pub async fn check_person_block( diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 4642078dd..5bb6f55b1 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -10,10 +10,11 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ comment::{Comment, CommentUpdateForm}, + comment_report::CommentReport, moderator::{ModRemoveComment, ModRemoveCommentForm}, post::Post, }, - traits::Crud, + traits::{Crud, Reportable}, }; use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; @@ -48,6 +49,9 @@ pub async fn remove_comment( .await .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; + CommentReport::resolve_all_for_object(&mut context.pool(), comment_id, local_user_view.person.id) + .await?; + // Mod tables let form = ModRemoveCommentForm { mod_person_id: local_user_view.person.id, diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs index 5b3b33a2e..cbcf069b6 100644 --- a/crates/api_crud/src/post/remove.rs +++ b/crates/api_crud/src/post/remove.rs @@ -11,8 +11,9 @@ use lemmy_db_schema::{ source::{ moderator::{ModRemovePost, ModRemovePostForm}, post::{Post, PostUpdateForm}, + post_report::PostReport, }, - traits::Crud, + traits::{Crud, Reportable}, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; @@ -47,6 +48,9 @@ pub async fn remove_post( ) .await?; + PostReport::resolve_all_for_object(&mut context.pool(), post_id, local_user_view.person.id) + .await?; + // Mod tables let form = ModRemovePostForm { mod_person_id: local_user_view.person.id, diff --git a/crates/apub/src/activities/community/report.rs b/crates/apub/src/activities/community/report.rs index 11941675a..6b1fce066 100644 --- a/crates/apub/src/activities/community/report.rs +++ b/crates/apub/src/activities/community/report.rs @@ -14,7 +14,10 @@ use activitypub_federation::{ kinds::activity::FlagType, traits::{ActivityHandler, Actor}, }; -use lemmy_api_common::context::LemmyContext; +use lemmy_api_common::{ + context::LemmyContext, + utils::{check_comment_deleted_or_removed, check_post_deleted_or_removed}, +}; use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, @@ -104,6 +107,8 @@ impl ActivityHandler for Report { let reason = self.reason()?; match self.object.dereference(context).await? { PostOrComment::Post(post) => { + check_post_deleted_or_removed(&post)?; + let report_form = PostReportForm { creator_id: actor.id, post_id: post.id, @@ -115,6 +120,8 @@ impl ActivityHandler for Report { PostReport::report(&mut context.pool(), &report_form).await?; } PostOrComment::Comment(comment) => { + check_comment_deleted_or_removed(&comment)?; + let report_form = CommentReportForm { creator_id: actor.id, comment_id: comment.id, diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index 875dc9e89..18f8cf6fb 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -12,6 +12,7 @@ use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::{ source::{ comment::{Comment, CommentUpdateForm}, + comment_report::CommentReport, community::{Community, CommunityUpdateForm}, moderator::{ ModRemoveComment, @@ -22,8 +23,9 @@ use lemmy_db_schema::{ ModRemovePostForm, }, post::{Post, PostUpdateForm}, + post_report::PostReport, }, - traits::Crud, + traits::{Crud, Reportable}, }; use lemmy_utils::error::{LemmyError, LemmyErrorType}; use url::Url; @@ -136,6 +138,7 @@ pub(in crate::activities) async fn receive_remove_action( .await?; } DeletableObjects::Post(post) => { + PostReport::resolve_all_for_object(&mut context.pool(), post.id, actor.id).await?; let form = ModRemovePostForm { mod_person_id: actor.id, post_id: post.id, @@ -154,6 +157,7 @@ pub(in crate::activities) async fn receive_remove_action( .await?; } DeletableObjects::Comment(comment) => { + CommentReport::resolve_all_for_object(&mut context.pool(), comment.id, actor.id).await?; let form = ModRemoveCommentForm { mod_person_id: actor.id, comment_id: comment.id, diff --git a/crates/db_schema/src/impls/comment_report.rs b/crates/db_schema/src/impls/comment_report.rs index ff93915e1..19c12876f 100644 --- a/crates/db_schema/src/impls/comment_report.rs +++ b/crates/db_schema/src/impls/comment_report.rs @@ -1,6 +1,9 @@ use crate::{ - newtypes::{CommentReportId, PersonId}, - schema::comment_report::dsl::{comment_report, resolved, resolver_id, updated}, + newtypes::{CommentId, CommentReportId, PersonId}, + schema::comment_report::{ + comment_id, + dsl::{comment_report, resolved, resolver_id, updated}, + }, source::comment_report::{CommentReport, CommentReportForm}, traits::Reportable, utils::{get_conn, naive_now, DbPool}, @@ -17,6 +20,7 @@ use diesel_async::RunQueryDsl; impl Reportable for CommentReport { type Form = CommentReportForm; type IdType = CommentReportId; + type ObjectIdType = CommentId; /// creates a comment report and returns it /// /// * `conn` - the postgres connection @@ -53,6 +57,22 @@ impl Reportable for CommentReport { .await } + async fn resolve_all_for_object( + pool: &mut DbPool<'_>, + comment_id_: CommentId, + by_resolver_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + update(comment_report.filter(comment_id.eq(comment_id_))) + .set(( + resolved.eq(true), + resolver_id.eq(by_resolver_id), + updated.eq(naive_now()), + )) + .execute(conn) + .await + } + /// unresolve a comment report /// /// * `conn` - the postgres connection diff --git a/crates/db_schema/src/impls/post_report.rs b/crates/db_schema/src/impls/post_report.rs index b0071f965..face766db 100644 --- a/crates/db_schema/src/impls/post_report.rs +++ b/crates/db_schema/src/impls/post_report.rs @@ -1,6 +1,9 @@ use crate::{ - newtypes::{PersonId, PostReportId}, - schema::post_report::dsl::{post_report, resolved, resolver_id, updated}, + newtypes::{PersonId, PostId, PostReportId}, + schema::post_report::{ + dsl::{post_report, resolved, resolver_id, updated}, + post_id, + }, source::post_report::{PostReport, PostReportForm}, traits::Reportable, utils::{get_conn, naive_now, DbPool}, @@ -17,6 +20,7 @@ use diesel_async::RunQueryDsl; impl Reportable for PostReport { type Form = PostReportForm; type IdType = PostReportId; + type ObjectIdType = PostId; async fn report(pool: &mut DbPool<'_>, post_report_form: &PostReportForm) -> Result { let conn = &mut get_conn(pool).await?; @@ -42,6 +46,22 @@ impl Reportable for PostReport { .await } + async fn resolve_all_for_object( + pool: &mut DbPool<'_>, + post_id_: PostId, + by_resolver_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + update(post_report.filter(post_id.eq(post_id_))) + .set(( + resolved.eq(true), + resolver_id.eq(by_resolver_id), + updated.eq(naive_now()), + )) + .execute(conn) + .await + } + async fn unresolve( pool: &mut DbPool<'_>, report_id: Self::IdType, @@ -75,7 +95,6 @@ mod tests { traits::Crud, utils::build_db_pool_for_tests, }; - use pretty_assertions::assert_eq; use serial_test::serial; async fn init(pool: &mut DbPool<'_>) -> (Person, PostReport) { @@ -135,4 +154,21 @@ mod tests { Person::delete(pool, person.id).await.unwrap(); Post::delete(pool, report.post_id).await.unwrap(); } + + #[tokio::test] + #[serial] + async fn test_resolve_all_post_reports() { + let pool = &build_db_pool_for_tests().await; + let pool = &mut pool.into(); + + let (person, report) = init(pool).await; + + let resolved_count = PostReport::resolve_all_for_object(pool, report.post_id, person.id) + .await + .unwrap(); + assert_eq!(resolved_count, 1); + + Person::delete(pool, person.id).await.unwrap(); + Post::delete(pool, report.post_id).await.unwrap(); + } } diff --git a/crates/db_schema/src/impls/private_message_report.rs b/crates/db_schema/src/impls/private_message_report.rs index ca2187960..c20783db0 100644 --- a/crates/db_schema/src/impls/private_message_report.rs +++ b/crates/db_schema/src/impls/private_message_report.rs @@ -1,5 +1,5 @@ use crate::{ - newtypes::{PersonId, PrivateMessageReportId}, + newtypes::{PersonId, PrivateMessageId, PrivateMessageReportId}, schema::private_message_report::dsl::{private_message_report, resolved, resolver_id, updated}, source::private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, traits::Reportable, @@ -17,6 +17,7 @@ use diesel_async::RunQueryDsl; impl Reportable for PrivateMessageReport { type Form = PrivateMessageReportForm; type IdType = PrivateMessageReportId; + type ObjectIdType = PrivateMessageId; async fn report( pool: &mut DbPool<'_>, @@ -45,6 +46,15 @@ impl Reportable for PrivateMessageReport { .await } + // TODO: this is unused because private message doesnt have remove handler + async fn resolve_all_for_object( + _pool: &mut DbPool<'_>, + _pm_id_: PrivateMessageId, + _by_resolver_id: PersonId, + ) -> Result { + unimplemented!() + } + async fn unresolve( pool: &mut DbPool<'_>, report_id: Self::IdType, diff --git a/crates/db_schema/src/traits.rs b/crates/db_schema/src/traits.rs index e58319c0b..b0434a65c 100644 --- a/crates/db_schema/src/traits.rs +++ b/crates/db_schema/src/traits.rs @@ -144,6 +144,7 @@ pub trait Blockable { pub trait Reportable { type Form; type IdType; + type ObjectIdType; async fn report(pool: &mut DbPool<'_>, form: &Self::Form) -> Result where Self: Sized; @@ -152,6 +153,13 @@ pub trait Reportable { report_id: Self::IdType, resolver_id: PersonId, ) -> Result + where + Self: Sized; + async fn resolve_all_for_object( + pool: &mut DbPool<'_>, + comment_id_: Self::ObjectIdType, + by_resolver_id: PersonId, + ) -> Result where Self: Sized; async fn unresolve( diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index c503ae81a..b7c5df6a9 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -201,7 +201,6 @@ mod tests { community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, instance::Instance, local_user::{LocalUser, LocalUserInsertForm}, - moderator::{ModRemovePost, ModRemovePostForm}, person::{Person, PersonInsertForm}, post::{Post, PostInsertForm}, post_report::{PostReport, PostReportForm}, @@ -350,14 +349,11 @@ mod tests { .unwrap(); assert_eq!(2, report_count); - // Writing post removal to mod log should automatically resolve reports - let remove_form = ModRemovePostForm { - mod_person_id: inserted_timmy.id, - post_id: inserted_jessica_report.post_id, - reason: None, - removed: Some(true), - }; - ModRemovePost::create(pool, &remove_form).await.unwrap(); + // Pretend the post was removed, and resolve all reports for that object. + // This is called manually in the API for post removals + PostReport::resolve_all_for_object(pool, inserted_jessica_report.post_id, inserted_timmy.id) + .await + .unwrap(); let read_jessica_report_view_after_resolve = PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id) diff --git a/migrations/2024-01-25-151400_remove_auto_resolve_report_trigger/down.sql b/migrations/2024-01-25-151400_remove_auto_resolve_report_trigger/down.sql new file mode 100644 index 000000000..101fe441c --- /dev/null +++ b/migrations/2024-01-25-151400_remove_auto_resolve_report_trigger/down.sql @@ -0,0 +1,48 @@ +-- Automatically resolve all reports for a given post once it is marked as removed +CREATE OR REPLACE FUNCTION post_removed_resolve_reports () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + post_report + SET + resolved = TRUE, + resolver_id = NEW.mod_person_id, + updated = now() + WHERE + post_report.post_id = NEW.post_id; + RETURN NULL; +END +$$; + +CREATE OR REPLACE TRIGGER post_removed_resolve_reports + AFTER INSERT ON mod_remove_post + FOR EACH ROW + WHEN (NEW.removed) + EXECUTE PROCEDURE post_removed_resolve_reports (); + +-- Same when comment is marked as removed +CREATE OR REPLACE FUNCTION comment_removed_resolve_reports () + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ +BEGIN + UPDATE + comment_report + SET + resolved = TRUE, + resolver_id = NEW.mod_person_id, + updated = now() + WHERE + comment_report.comment_id = NEW.comment_id; + RETURN NULL; +END +$$; + +CREATE OR REPLACE TRIGGER comment_removed_resolve_reports + AFTER INSERT ON mod_remove_comment + FOR EACH ROW + WHEN (NEW.removed) + EXECUTE PROCEDURE comment_removed_resolve_reports (); + diff --git a/migrations/2024-01-25-151400_remove_auto_resolve_report_trigger/up.sql b/migrations/2024-01-25-151400_remove_auto_resolve_report_trigger/up.sql new file mode 100644 index 000000000..0ea58fc3c --- /dev/null +++ b/migrations/2024-01-25-151400_remove_auto_resolve_report_trigger/up.sql @@ -0,0 +1,8 @@ +DROP TRIGGER IF EXISTS post_removed_resolve_reports ON mod_remove_post; + +DROP FUNCTION IF EXISTS post_removed_resolve_reports; + +DROP TRIGGER IF EXISTS comment_removed_resolve_reports ON mod_remove_comment; + +DROP FUNCTION IF EXISTS comment_removed_resolve_reports; + From 86b44c2a4dcde8f2dd2db33af56824b7a0ef4119 Mon Sep 17 00:00:00 2001 From: Nutomic Date: Fri, 16 Feb 2024 13:24:35 +0100 Subject: [PATCH 06/12] Add site.content_warning, local_site.default_post_listing_mode (#4393) * Include local_site.content_warning setting for showing nsfw by default * Add community setting `only_followers_can_vote` * clippy * add auto_expand_images site setting * cleanup * add missing api params * postquery/communityquery changes * clippy * change error * replace auto_expand_images with default_site_post_listing_mode * change post/community query params * get rid of only_followers_can_vote * machete * fix * clippy * revert remaining vote changes * remove dead code * remove unused var * fmt --- Cargo.lock | 3 + crates/api_common/src/site.rs | 8 ++ crates/api_crud/src/community/list.rs | 9 +- crates/api_crud/src/post/read.rs | 10 +- crates/api_crud/src/site/create.rs | 4 + crates/api_crud/src/site/update.rs | 4 + crates/api_crud/src/user/create.rs | 1 + crates/apub/src/api/list_posts.rs | 12 +-- crates/apub/src/api/read_person.rs | 15 +-- crates/apub/src/api/search.rs | 24 ++--- crates/apub/src/objects/instance.rs | 2 + crates/apub/src/protocol/objects/instance.rs | 2 + crates/db_perf/Cargo.toml | 1 + crates/db_perf/src/main.rs | 24 ++++- crates/db_schema/src/impls/community.rs | 4 +- crates/db_schema/src/lib.rs | 5 +- crates/db_schema/src/schema.rs | 3 + crates/db_schema/src/source/local_site.rs | 5 + crates/db_schema/src/source/site.rs | 5 + crates/db_views/Cargo.toml | 1 + crates/db_views/src/post_view.rs | 102 ++++++++++++------ crates/db_views_actor/Cargo.toml | 1 + crates/db_views_actor/src/community_view.rs | 43 ++++++-- crates/routes/src/feeds.rs | 8 +- .../down.sql | 6 ++ .../up.sql | 6 ++ 26 files changed, 221 insertions(+), 87 deletions(-) create mode 100644 migrations/2024-01-22-105746_lemmynsfw-changes/down.sql create mode 100644 migrations/2024-01-22-105746_lemmynsfw-changes/up.sql diff --git a/Cargo.lock b/Cargo.lock index abe673cd8..d59485e76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2683,6 +2683,7 @@ dependencies = [ "lemmy_db_views", "lemmy_utils", "tokio", + "url", ] [[package]] @@ -2743,6 +2744,7 @@ dependencies = [ "tokio", "tracing", "ts-rs", + "url", ] [[package]] @@ -2761,6 +2763,7 @@ dependencies = [ "strum_macros", "tokio", "ts-rs", + "url", ] [[package]] diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 3f02663c5..b92dd40ce 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -10,6 +10,7 @@ use lemmy_db_schema::{ }, ListingType, ModlogActionType, + PostListingMode, RegistrationMode, SearchType, SortType, @@ -187,6 +188,8 @@ pub struct CreateSite { pub blocked_instances: Option>, pub taglines: Option>, pub registration_mode: Option, + pub content_warning: Option, + pub default_post_listing_mode: Option, } #[skip_serializing_none] @@ -265,6 +268,11 @@ pub struct EditSite { pub registration_mode: Option, /// Whether to email admins for new reports. pub reports_email_admins: Option, + /// If present, nsfw content is visible by default. Should be displayed by frontends/clients + /// when the site is first opened by a user. + pub content_warning: Option, + /// Default value for [LocalUser.post_listing_mode] + pub default_post_listing_mode: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/crates/api_crud/src/community/list.rs b/crates/api_crud/src/community/list.rs index 0879421ba..7990352fc 100644 --- a/crates/api_crud/src/community/list.rs +++ b/crates/api_crud/src/community/list.rs @@ -4,8 +4,7 @@ use lemmy_api_common::{ context::LemmyContext, utils::{check_private_instance, is_admin}, }; -use lemmy_db_schema::source::local_site::LocalSite; -use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views_actor::community_view::CommunityQuery; use lemmy_utils::error::LemmyError; @@ -15,13 +14,13 @@ pub async fn list_communities( context: Data, local_user_view: Option, ) -> Result, LemmyError> { - let local_site = LocalSite::read(&mut context.pool()).await?; + let local_site = SiteView::read_local(&mut context.pool()).await?; let is_admin = local_user_view .as_ref() .map(|luv| is_admin(luv).is_ok()) .unwrap_or_default(); - check_private_instance(&local_user_view, &local_site)?; + check_private_instance(&local_user_view, &local_site.local_site)?; let sort = data.sort; let listing_type = data.type_; @@ -39,7 +38,7 @@ pub async fn list_communities( is_mod_or_admin: is_admin, ..Default::default() } - .list(&mut context.pool()) + .list(&local_site.site, &mut context.pool()) .await?; // Return the jwt diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs index 352f97fe1..e701008b7 100644 --- a/crates/api_crud/src/post/read.rs +++ b/crates/api_crud/src/post/read.rs @@ -6,12 +6,12 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, - source::{comment::Comment, local_site::LocalSite, post::Post}, + source::{comment::Comment, post::Post}, traits::Crud, }; use lemmy_db_views::{ post_view::PostQuery, - structs::{LocalUserView, PostView}, + structs::{LocalUserView, PostView, SiteView}, }; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; @@ -22,9 +22,9 @@ pub async fn get_post( context: Data, local_user_view: Option, ) -> Result, LemmyError> { - let local_site = LocalSite::read(&mut context.pool()).await?; + let local_site = SiteView::read_local(&mut context.pool()).await?; - check_private_instance(&local_user_view, &local_site)?; + check_private_instance(&local_user_view, &local_site.local_site)?; let person_id = local_user_view.as_ref().map(|u| u.person.id); @@ -93,7 +93,7 @@ pub async fn get_post( url_search: Some(url.inner().as_str().into()), ..Default::default() } - .list(&mut context.pool()) + .list(&local_site.site, &mut context.pool()) .await?; // Don't return this post as one of the cross_posts diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 994375027..88e91a694 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -73,6 +73,7 @@ pub async fn create_site( inbox_url, private_key: Some(Some(keypair.private_key)), public_key: Some(keypair.public_key), + content_warning: diesel_option_overwrite(data.content_warning.clone()), ..Default::default() }; @@ -101,6 +102,7 @@ pub async fn create_site( federation_enabled: data.federation_enabled, captcha_enabled: data.captcha_enabled, captcha_difficulty: data.captcha_difficulty.clone(), + default_post_listing_mode: data.default_post_listing_mode, ..Default::default() }; @@ -568,6 +570,8 @@ mod tests { blocked_instances: None, taglines: None, registration_mode: site_registration_mode, + content_warning: None, + default_post_listing_mode: None, } } } diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index ba716c9a8..46bd1e49f 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -71,6 +71,7 @@ pub async fn update_site( description: diesel_option_overwrite(data.description.clone()), icon, banner, + content_warning: diesel_option_overwrite(data.content_warning.clone()), updated: Some(Some(naive_now())), ..Default::default() }; @@ -101,6 +102,7 @@ pub async fn update_site( captcha_enabled: data.captcha_enabled, captcha_difficulty: data.captcha_difficulty.clone(), reports_email_admins: data.reports_email_admins, + default_post_listing_mode: data.default_post_listing_mode, ..Default::default() }; @@ -566,6 +568,8 @@ mod tests { taglines: None, registration_mode: site_registration_mode, reports_email_admins: None, + content_warning: None, + default_post_listing_mode: None, } } } diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 5f11384f7..50df1edbf 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -142,6 +142,7 @@ pub async fn register( .show_nsfw(Some(data.show_nsfw)) .accepted_application(accepted_application) .default_listing_type(Some(local_site.default_post_listing_type)) + .post_listing_mode(Some(local_site.default_post_listing_mode)) .interface_language(language_tag) // If its the initial site setup, they are an admin .admin(Some(!local_site.site_setup)) diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index dc3618c50..d4ed566c4 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -10,10 +10,10 @@ use lemmy_api_common::{ post::{GetPosts, GetPostsResponse}, utils::check_private_instance, }; -use lemmy_db_schema::source::{community::Community, local_site::LocalSite}; +use lemmy_db_schema::source::community::Community; use lemmy_db_views::{ post_view::PostQuery, - structs::{LocalUserView, PaginationCursor}, + structs::{LocalUserView, PaginationCursor, SiteView}, }; use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; @@ -23,9 +23,9 @@ pub async fn list_posts( context: Data, local_user_view: Option, ) -> Result, LemmyError> { - let local_site = LocalSite::read(&mut context.pool()).await?; + let local_site = SiteView::read_local(&mut context.pool()).await?; - check_private_instance(&local_user_view, &local_site)?; + check_private_instance(&local_user_view, &local_site.local_site)?; let sort = data.sort; @@ -47,7 +47,7 @@ pub async fn list_posts( let listing_type = Some(listing_type_with_default( data.type_, - &local_site, + &local_site.local_site, community_id, )?); // parse pagination token @@ -70,7 +70,7 @@ pub async fn list_posts( limit, ..Default::default() } - .list(&mut context.pool()) + .list(&local_site.site, &mut context.pool()) .await .with_lemmy_type(LemmyErrorType::CouldntGetPosts)?; diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index 8108c3474..c779657c8 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -6,11 +6,12 @@ use lemmy_api_common::{ person::{GetPersonDetails, GetPersonDetailsResponse}, utils::{check_private_instance, read_site_for_actor}, }; -use lemmy_db_schema::{ - source::{local_site::LocalSite, person::Person}, - utils::post_to_comment_sort_type, +use lemmy_db_schema::{source::person::Person, utils::post_to_comment_sort_type}; +use lemmy_db_views::{ + comment_view::CommentQuery, + post_view::PostQuery, + structs::{LocalUserView, SiteView}, }; -use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery, structs::LocalUserView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonView}; use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType}; @@ -25,9 +26,9 @@ pub async fn read_person( Err(LemmyErrorType::NoIdGiven)? } - let local_site = LocalSite::read(&mut context.pool()).await?; + let local_site = SiteView::read_local(&mut context.pool()).await?; - check_private_instance(&local_user_view, &local_site)?; + check_private_instance(&local_user_view, &local_site.local_site)?; let person_details_id = match data.person_id { Some(id) => id, @@ -70,7 +71,7 @@ pub async fn read_person( creator_id, ..Default::default() } - .list(&mut context.pool()) + .list(&local_site.site, &mut context.pool()) .await?; let comments = CommentQuery { diff --git a/crates/apub/src/api/search.rs b/crates/apub/src/api/search.rs index 96ff97f70..dff2dffeb 100644 --- a/crates/apub/src/api/search.rs +++ b/crates/apub/src/api/search.rs @@ -6,12 +6,12 @@ use lemmy_api_common::{ site::{Search, SearchResponse}, utils::{check_private_instance, is_admin}, }; -use lemmy_db_schema::{ - source::{community::Community, local_site::LocalSite}, - utils::post_to_comment_sort_type, - SearchType, +use lemmy_db_schema::{source::community::Community, utils::post_to_comment_sort_type, SearchType}; +use lemmy_db_views::{ + comment_view::CommentQuery, + post_view::PostQuery, + structs::{LocalUserView, SiteView}, }; -use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery, structs::LocalUserView}; use lemmy_db_views_actor::{community_view::CommunityQuery, person_view::PersonQuery}; use lemmy_utils::error::LemmyError; @@ -21,9 +21,9 @@ pub async fn search( context: Data, local_user_view: Option, ) -> Result, LemmyError> { - let local_site = LocalSite::read(&mut context.pool()).await?; + let local_site = SiteView::read_local(&mut context.pool()).await?; - check_private_instance(&local_user_view, &local_site)?; + check_private_instance(&local_user_view, &local_site.local_site)?; let is_admin = local_user_view .as_ref() @@ -68,7 +68,7 @@ pub async fn search( limit: (limit), ..Default::default() } - .list(&mut context.pool()) + .list(&local_site.site, &mut context.pool()) .await?; } SearchType::Comments => { @@ -97,7 +97,7 @@ pub async fn search( limit: (limit), ..Default::default() } - .list(&mut context.pool()) + .list(&local_site.site, &mut context.pool()) .await?; } SearchType::Users => { @@ -128,7 +128,7 @@ pub async fn search( limit: (limit), ..Default::default() } - .list(&mut context.pool()) + .list(&local_site.site, &mut context.pool()) .await?; let q = data.q.clone(); @@ -162,7 +162,7 @@ pub async fn search( limit: (limit), ..Default::default() } - .list(&mut context.pool()) + .list(&local_site.site, &mut context.pool()) .await? }; @@ -192,7 +192,7 @@ pub async fn search( limit: (limit), ..Default::default() } - .list(&mut context.pool()) + .list(&local_site.site, &mut context.pool()) .await?; } }; diff --git a/crates/apub/src/objects/instance.rs b/crates/apub/src/objects/instance.rs index c7d4f11f6..61a70f6ea 100644 --- a/crates/apub/src/objects/instance.rs +++ b/crates/apub/src/objects/instance.rs @@ -106,6 +106,7 @@ impl Object for ApubSite { outbox: Url::parse(&format!("{}/site_outbox", self.actor_id))?, public_key: self.public_key(), language, + content_warning: self.content_warning.clone(), published: self.published, updated: self.updated, }; @@ -154,6 +155,7 @@ impl Object for ApubSite { public_key: Some(apub.public_key.public_key_pem.clone()), private_key: None, instance_id: instance.id, + content_warning: apub.content_warning, }; let languages = LanguageTag::to_language_id_multiple(apub.language, &mut context.pool()).await?; diff --git a/crates/apub/src/protocol/objects/instance.rs b/crates/apub/src/protocol/objects/instance.rs index 8c9944306..17623719c 100644 --- a/crates/apub/src/protocol/objects/instance.rs +++ b/crates/apub/src/protocol/objects/instance.rs @@ -39,6 +39,8 @@ pub struct Instance { pub(crate) image: Option, #[serde(default)] pub(crate) language: Vec, + /// nonstandard field + pub(crate) content_warning: Option, pub(crate) published: DateTime, pub(crate) updated: Option>, } diff --git a/crates/db_perf/Cargo.toml b/crates/db_perf/Cargo.toml index 87d2a58ac..1e74b4966 100644 --- a/crates/db_perf/Cargo.toml +++ b/crates/db_perf/Cargo.toml @@ -21,3 +21,4 @@ lemmy_db_schema = { workspace = true } lemmy_db_views = { workspace = true, features = ["full"] } lemmy_utils = { workspace = true } tokio = { workspace = true } +url = { workspace = true } diff --git a/crates/db_perf/src/main.rs b/crates/db_perf/src/main.rs index 2da9f9574..6139fc18e 100644 --- a/crates/db_perf/src/main.rs +++ b/crates/db_perf/src/main.rs @@ -16,6 +16,7 @@ use lemmy_db_schema::{ community::{Community, CommunityInsertForm}, instance::Instance, person::{Person, PersonInsertForm}, + site::Site, }, traits::Crud, utils::{build_db_pool, get_conn, now}, @@ -24,6 +25,7 @@ use lemmy_db_schema::{ use lemmy_db_views::{post_view::PostQuery, structs::PaginationCursor}; use lemmy_utils::error::{LemmyErrorExt2, LemmyResult}; use std::num::NonZeroU32; +use url::Url; #[derive(Parser, Debug)] struct CmdArgs { @@ -157,7 +159,7 @@ async fn try_main() -> LemmyResult<()> { page_after, ..Default::default() } - .list(&mut conn.into()) + .list(&site()?, &mut conn.into()) .await?; if let Some(post_view) = post_views.into_iter().last() { @@ -181,3 +183,23 @@ async fn try_main() -> LemmyResult<()> { Ok(()) } + +fn site() -> LemmyResult { + Ok(Site { + id: Default::default(), + name: String::new(), + sidebar: None, + published: Default::default(), + updated: None, + icon: None, + banner: None, + description: None, + actor_id: Url::parse("http://example.com")?.into(), + last_refreshed_at: Default::default(), + inbox_url: Url::parse("http://example.com")?.into(), + private_key: None, + public_key: String::new(), + instance_id: Default::default(), + content_warning: None, + }) +} diff --git a/crates/db_schema/src/impls/community.rs b/crates/db_schema/src/impls/community.rs index 0745e3b25..3298da894 100644 --- a/crates/db_schema/src/impls/community.rs +++ b/crates/db_schema/src/impls/community.rs @@ -22,9 +22,10 @@ use crate::{ use diesel::{ deserialize, dsl, - dsl::insert_into, + dsl::{exists, insert_into}, pg::Pg, result::Error, + select, sql_types, ExpressionMethods, NullableExpressionMethods, @@ -235,7 +236,6 @@ impl CommunityFollower { remote_community_id: CommunityId, ) -> Result { use crate::schema::community_follower::dsl::{community_follower, community_id}; - use diesel::dsl::{exists, select}; let conn = &mut get_conn(pool).await?; select(exists( community_follower.filter(community_id.eq(remote_community_id)), diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 7c1a6adc2..05663ff3e 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -139,7 +139,9 @@ pub enum RegistrationMode { Open, } -#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] +#[derive( + EnumString, Display, Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Hash, +)] #[cfg_attr(feature = "full", derive(DbEnum, TS))] #[cfg_attr( feature = "full", @@ -150,6 +152,7 @@ pub enum RegistrationMode { /// A post-view mode that changes how multiple post listings look. pub enum PostListingMode { /// A compact, list-type view. + #[default] List, /// A larger card-type view. Card, diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 195e99660..de54c379c 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -353,6 +353,7 @@ diesel::table! { use diesel::sql_types::*; use super::sql_types::ListingTypeEnum; use super::sql_types::RegistrationModeEnum; + use super::sql_types::PostListingModeEnum; local_site (id) { id -> Int4, @@ -380,6 +381,7 @@ diesel::table! { registration_mode -> RegistrationModeEnum, reports_email_admins -> Bool, federation_signed_fetch -> Bool, + default_post_listing_mode -> PostListingModeEnum, } } @@ -869,6 +871,7 @@ diesel::table! { private_key -> Nullable, public_key -> Text, instance_id -> Int4, + content_warning -> Nullable, } } diff --git a/crates/db_schema/src/source/local_site.rs b/crates/db_schema/src/source/local_site.rs index ac545d574..ea3dbc179 100644 --- a/crates/db_schema/src/source/local_site.rs +++ b/crates/db_schema/src/source/local_site.rs @@ -3,6 +3,7 @@ use crate::schema::local_site; use crate::{ newtypes::{LocalSiteId, SiteId}, ListingType, + PostListingMode, RegistrationMode, }; use chrono::{DateTime, Utc}; @@ -64,6 +65,8 @@ pub struct LocalSite { /// Whether to sign outgoing Activitypub fetches with private key of local instance. Some /// Fediverse instances and platforms require this. pub federation_signed_fetch: bool, + /// Default value for [LocalUser.post_listing_mode] + pub default_post_listing_mode: PostListingMode, } #[derive(Clone, TypedBuilder)] @@ -93,6 +96,7 @@ pub struct LocalSiteInsertForm { pub registration_mode: Option, pub reports_email_admins: Option, pub federation_signed_fetch: Option, + pub default_post_listing_mode: Option, } #[derive(Clone, Default)] @@ -120,4 +124,5 @@ pub struct LocalSiteUpdateForm { pub reports_email_admins: Option, pub updated: Option>>, pub federation_signed_fetch: Option, + pub default_post_listing_mode: Option, } diff --git a/crates/db_schema/src/source/site.rs b/crates/db_schema/src/source/site.rs index 14b931847..754acb2aa 100644 --- a/crates/db_schema/src/source/site.rs +++ b/crates/db_schema/src/source/site.rs @@ -37,6 +37,9 @@ pub struct Site { pub private_key: Option, pub public_key: String, pub instance_id: InstanceId, + /// If present, nsfw content is visible by default. Should be displayed by frontends/clients + /// when the site is first opened by a user. + pub content_warning: Option, } #[derive(Clone, TypedBuilder)] @@ -58,6 +61,7 @@ pub struct SiteInsertForm { pub public_key: Option, #[builder(!default)] pub instance_id: InstanceId, + pub content_warning: Option, } #[derive(Clone, Default)] @@ -76,4 +80,5 @@ pub struct SiteUpdateForm { pub inbox_url: Option, pub private_key: Option>, pub public_key: Option, + pub content_warning: Option>, } diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml index 3f0ba5aff..cdd44869c 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -45,3 +45,4 @@ serial_test = { workspace = true } tokio = { workspace = true } chrono = { workspace = true } pretty_assertions = { workspace = true } +url = { workspace = true } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index 6c5a983b1..6e15d1678 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -39,6 +39,7 @@ use lemmy_db_schema::{ post_read, post_saved, }, + source::site::Site, utils::{ functions::coalesce, fuzzy_search, @@ -61,7 +62,7 @@ use tracing::debug; fn queries<'a>() -> Queries< impl ReadFn<'a, PostView, (PostId, Option, bool)>, - impl ListFn<'a, PostView, PostQuery<'a>>, + impl ListFn<'a, PostView, (PostQuery<'a>, &'a Site)>, > { let is_creator_banned_from_community = exists( community_person_ban::table.filter( @@ -270,7 +271,7 @@ fn queries<'a>() -> Queries< .await }; - let list = move |mut conn: DbConn<'a>, options: PostQuery<'a>| async move { + let list = move |mut conn: DbConn<'a>, (options, site): (PostQuery<'a>, &'a Site)| async move { let my_person_id = options.local_user.map(|l| l.person.id); let my_local_user_id = options.local_user.map(|l| l.local_user.id); @@ -368,10 +369,12 @@ fn queries<'a>() -> Queries< ); } + // If there is a content warning, show nsfw content by default. + let has_content_warning = site.content_warning.is_some(); if !options .local_user .map(|l| l.local_user.show_nsfw) - .unwrap_or(false) + .unwrap_or(has_content_warning) { query = query .filter(post::nsfw.eq(false)) @@ -571,7 +574,7 @@ impl PaginationCursor { #[derive(Clone)] pub struct PaginationCursorData(PostAggregates); -#[derive(Default, Clone)] +#[derive(Clone, Default)] pub struct PostQuery<'a> { pub listing_type: Option, pub sort: Option, @@ -595,6 +598,7 @@ pub struct PostQuery<'a> { impl<'a> PostQuery<'a> { async fn prefetch_upper_bound_for_page_before( &self, + site: &Site, pool: &mut DbPool<'_>, ) -> Result>, Error> { // first get one page for the most popular community to get an upper bound for the the page end for the real query @@ -645,11 +649,14 @@ impl<'a> PostQuery<'a> { let mut v = queries() .list( pool, - PostQuery { - community_id: Some(largest_subscribed), - community_id_just_for_prefetch: true, - ..self.clone() - }, + ( + PostQuery { + community_id: Some(largest_subscribed), + community_id_just_for_prefetch: true, + ..self.clone() + }, + site, + ), ) .await?; // take last element of array. if this query returned less than LIMIT elements, @@ -671,19 +678,22 @@ impl<'a> PostQuery<'a> { } } - pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { + pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { if self.listing_type == Some(ListingType::Subscribed) && self.community_id.is_none() && self.local_user.is_some() && self.page_before_or_equal.is_none() { - if let Some(query) = self.prefetch_upper_bound_for_page_before(pool).await? { - queries().list(pool, query).await + if let Some(query) = self + .prefetch_upper_bound_for_page_before(site, pool) + .await? + { + queries().list(pool, (query, site)).await } else { Ok(vec![]) } } else { - queries().list(pool, self).await + queries().list(pool, (self, site)).await } } } @@ -717,6 +727,7 @@ mod tests { person::{Person, PersonInsertForm}, person_block::{PersonBlock, PersonBlockForm}, post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm}, + site::Site, }, traits::{Blockable, Crud, Joinable, Likeable}, utils::{build_db_pool, build_db_pool_for_tests, DbPool, RANK_DEFAULT}, @@ -728,6 +739,7 @@ mod tests { use pretty_assertions::assert_eq; use serial_test::serial; use std::{collections::HashSet, time::Duration}; + use url::Url; const POST_BY_BLOCKED_PERSON: &str = "post by blocked person"; const POST_BY_BOT: &str = "post by bot"; @@ -745,6 +757,7 @@ mod tests { inserted_community: Community, inserted_post: Post, inserted_bot_post: Post, + site: Site, } impl Data { @@ -842,6 +855,24 @@ mod tests { counts: Default::default(), }; + let site = Site { + id: Default::default(), + name: String::new(), + sidebar: None, + published: Default::default(), + updated: None, + icon: None, + banner: None, + description: None, + actor_id: Url::parse("http://example.com")?.into(), + last_refreshed_at: Default::default(), + inbox_url: Url::parse("http://example.com")?.into(), + private_key: None, + public_key: String::new(), + instance_id: Default::default(), + content_warning: None, + }; + Ok(Data { inserted_instance, local_user_view, @@ -850,6 +881,7 @@ mod tests { inserted_community, inserted_post, inserted_bot_post, + site, }) } @@ -872,7 +904,7 @@ mod tests { community_id: Some(data.inserted_community.id), ..data.default_post_query() } - .list(pool) + .list(&data.site, pool) .await?; let post_listing_single_with_person = PostView::read( @@ -907,7 +939,7 @@ mod tests { community_id: Some(data.inserted_community.id), ..data.default_post_query() } - .list(pool) + .list(&data.site, pool) .await?; // should include bot post which has "undetermined" language assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_with_bots)); @@ -927,7 +959,7 @@ mod tests { local_user: None, ..data.default_post_query() } - .list(pool) + .list(&data.site, pool) .await?; let read_post_listing_single_no_person = @@ -970,7 +1002,7 @@ mod tests { community_id: Some(data.inserted_community.id), ..data.default_post_query() } - .list(pool) + .list(&data.site, pool) .await?; // Should be 0 posts after the community block assert_eq!(read_post_listings_with_person_after_block, vec![]); @@ -1028,7 +1060,7 @@ mod tests { community_id: Some(data.inserted_community.id), ..data.default_post_query() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!(vec![expected_post_with_upvote], read_post_listing); @@ -1037,7 +1069,7 @@ mod tests { liked_only: true, ..data.default_post_query() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!(read_post_listing, read_liked_post_listing); @@ -1046,7 +1078,7 @@ mod tests { disliked_only: true, ..data.default_post_query() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!(read_disliked_post_listing, vec![]); @@ -1076,7 +1108,7 @@ mod tests { community_id: Some(data.inserted_community.id), ..data.default_post_query() } - .list(pool) + .list(&data.site, pool) .await? .into_iter() .map(|p| (p.creator.name, p.creator_is_moderator, p.creator_is_admin)) @@ -1118,14 +1150,14 @@ mod tests { Post::create(pool, &post_spanish).await?; - let post_listings_all = data.default_post_query().list(pool).await?; + let post_listings_all = data.default_post_query().list(&data.site, pool).await?; // no language filters specified, all posts should be returned assert_eq!(vec![EL_POSTO, POST_BY_BOT, POST], names(&post_listings_all)); LocalUserLanguage::update(pool, vec![french_id], data.local_user_view.local_user.id).await?; - let post_listing_french = data.default_post_query().list(pool).await?; + let post_listing_french = data.default_post_query().list(&data.site, pool).await?; // only one post in french and one undetermined should be returned assert_eq!(vec![POST_BY_BOT, POST], names(&post_listing_french)); @@ -1142,7 +1174,7 @@ mod tests { .await?; let post_listings_french_und = data .default_post_query() - .list(pool) + .list(&data.site, pool) .await? .into_iter() .map(|p| (p.post.name, p.post.language_id)) @@ -1177,7 +1209,7 @@ mod tests { .await?; // Make sure you don't see the removed post in the results - let post_listings_no_admin = data.default_post_query().list(pool).await?; + let post_listings_no_admin = data.default_post_query().list(&data.site, pool).await?; assert_eq!(vec![POST], names(&post_listings_no_admin)); // Removed bot post is shown to admins on its profile page @@ -1186,7 +1218,7 @@ mod tests { creator_id: Some(data.inserted_bot.id), ..data.default_post_query() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!(vec![POST_BY_BOT], names(&post_listings_is_admin)); @@ -1221,7 +1253,7 @@ mod tests { local_user, ..data.default_post_query() } - .list(pool) + .list(&data.site, pool) .await? .iter() .any(|p| p.post.id == data.inserted_post.id); @@ -1261,7 +1293,7 @@ mod tests { let post_from_blocked_instance = Post::create(pool, &post_form).await?; // no instance block, should return all posts - let post_listings_all = data.default_post_query().list(pool).await?; + let post_listings_all = data.default_post_query().list(&data.site, pool).await?; assert_eq!( vec![POST_FROM_BLOCKED_INSTANCE, POST_BY_BOT, POST], names(&post_listings_all) @@ -1275,7 +1307,7 @@ mod tests { InstanceBlock::block(pool, &block_form).await?; // now posts from communities on that instance should be hidden - let post_listings_blocked = data.default_post_query().list(pool).await?; + let post_listings_blocked = data.default_post_query().list(&data.site, pool).await?; assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_blocked)); assert!(post_listings_blocked .iter() @@ -1283,7 +1315,7 @@ mod tests { // after unblocking it should return all posts again InstanceBlock::unblock(pool, &block_form).await?; - let post_listings_blocked = data.default_post_query().list(pool).await?; + let post_listings_blocked = data.default_post_query().list(&data.site, pool).await?; assert_eq!( vec![POST_FROM_BLOCKED_INSTANCE, POST_BY_BOT, POST], names(&post_listings_blocked) @@ -1351,7 +1383,7 @@ mod tests { page_after, ..options.clone() } - .list(pool) + .list(&data.site, pool) .await?; listed_post_ids.extend(post_listings.iter().map(|p| p.post.id)); @@ -1372,7 +1404,7 @@ mod tests { page_back: true, ..options.clone() } - .list(pool) + .list(&data.site, pool) .await?; let listed_post_ids = post_listings.iter().map(|p| p.post.id).collect::>(); @@ -1425,7 +1457,7 @@ mod tests { .await?; // Make sure you don't see the read post in the results - let post_listings_hide_read = data.default_post_query().list(pool).await?; + let post_listings_hide_read = data.default_post_query().list(&data.site, pool).await?; assert_eq!(vec![POST], names(&post_listings_hide_read)); cleanup(data, pool).await @@ -1577,7 +1609,7 @@ mod tests { let unauthenticated_query = PostQuery { ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!(0, unauthenticated_query.len()); @@ -1585,7 +1617,7 @@ mod tests { local_user: Some(&data.local_user_view), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await?; assert_eq!(2, authenticated_query.len()); diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml index 066b6bfd3..dd2b1aeb6 100644 --- a/crates/db_views_actor/Cargo.toml +++ b/crates/db_views_actor/Cargo.toml @@ -39,6 +39,7 @@ strum_macros = { workspace = true } serial_test = { workspace = true } tokio = { workspace = true } pretty_assertions = { workspace = true } +url.workspace = true [package.metadata.cargo-machete] ignored = ["strum"] diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index f9453ab8f..828738c27 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -20,7 +20,7 @@ use lemmy_db_schema::{ instance_block, local_user, }, - source::{community::CommunityFollower, local_user::LocalUser}, + source::{community::CommunityFollower, local_user::LocalUser, site::Site}, utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, CommunityVisibility, ListingType, @@ -29,7 +29,7 @@ use lemmy_db_schema::{ fn queries<'a>() -> Queries< impl ReadFn<'a, CommunityView, (CommunityId, Option, bool)>, - impl ListFn<'a, CommunityView, CommunityQuery<'a>>, + impl ListFn<'a, CommunityView, (CommunityQuery<'a>, &'a Site)>, > { let all_joins = |query: community::BoxedQuery<'a, Pg>, my_person_id: Option| { // The left join below will return None in this case @@ -96,7 +96,7 @@ fn queries<'a>() -> Queries< query.first::(&mut conn).await }; - let list = move |mut conn: DbConn<'a>, options: CommunityQuery<'a>| async move { + let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move { use SortType::*; let my_person_id = options.local_user.map(|l| l.person_id); @@ -158,8 +158,10 @@ fn queries<'a>() -> Queries< query = query.filter(community_block::person_id.is_null()); query = query.filter(community::nsfw.eq(false).or(local_user::show_nsfw.eq(true))); } else { - // No person in request, only show nsfw communities if show_nsfw is passed into request - if !options.show_nsfw { + // No person in request, only show nsfw communities if show_nsfw is passed into request or if + // site has content warning. + let has_content_warning = site.content_warning.is_some(); + if !options.show_nsfw && !has_content_warning { query = query.filter(community::nsfw.eq(false)); } // Hide local only communities from unauthenticated users @@ -233,8 +235,8 @@ pub struct CommunityQuery<'a> { } impl<'a> CommunityQuery<'a> { - pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, self).await + pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result, Error> { + queries().list(pool, (self, site)).await } } @@ -250,17 +252,20 @@ mod tests { instance::Instance, local_user::{LocalUser, LocalUserInsertForm}, person::{Person, PersonInsertForm}, + site::Site, }, traits::Crud, utils::{build_db_pool_for_tests, DbPool}, CommunityVisibility, }; use serial_test::serial; + use url::Url; struct Data { inserted_instance: Instance, local_user: LocalUser, inserted_community: Community, + site: Site, } async fn init_data(pool: &mut DbPool<'_>) -> Data { @@ -293,10 +298,30 @@ mod tests { let inserted_community = Community::create(pool, &new_community).await.unwrap(); + let url = Url::parse("http://example.com").unwrap(); + let site = Site { + id: Default::default(), + name: String::new(), + sidebar: None, + published: Default::default(), + updated: None, + icon: None, + banner: None, + description: None, + actor_id: url.clone().into(), + last_refreshed_at: Default::default(), + inbox_url: url.into(), + private_key: None, + public_key: String::new(), + instance_id: Default::default(), + content_warning: None, + }; + Data { inserted_instance, local_user, inserted_community, + site, } } @@ -333,7 +358,7 @@ mod tests { let unauthenticated_query = CommunityQuery { ..Default::default() } - .list(pool) + .list(&data.site, pool) .await .unwrap(); assert_eq!(0, unauthenticated_query.len()); @@ -342,7 +367,7 @@ mod tests { local_user: Some(&data.local_user), ..Default::default() } - .list(pool) + .list(&data.site, pool) .await .unwrap(); assert_eq!(1, authenticated_query.len()); diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 490fce4ae..b8ca2d5a6 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -163,7 +163,7 @@ async fn get_feed_data( page: (Some(page)), ..Default::default() } - .list(&mut context.pool()) + .list(&site_view.site, &mut context.pool()) .await?; let items = create_post_items(posts, &context.settings().get_protocol_and_hostname())?; @@ -270,7 +270,7 @@ async fn get_feed_user( page: (Some(*page)), ..Default::default() } - .list(&mut context.pool()) + .list(&site_view.site, &mut context.pool()) .await?; let items = create_post_items(posts, &context.settings().get_protocol_and_hostname())?; @@ -308,7 +308,7 @@ async fn get_feed_community( page: (Some(*page)), ..Default::default() } - .list(&mut context.pool()) + .list(&site_view.site, &mut context.pool()) .await?; let items = create_post_items(posts, &context.settings().get_protocol_and_hostname())?; @@ -349,7 +349,7 @@ async fn get_feed_front( page: (Some(*page)), ..Default::default() } - .list(&mut context.pool()) + .list(&site_view.site, &mut context.pool()) .await?; let protocol_and_hostname = context.settings().get_protocol_and_hostname(); diff --git a/migrations/2024-01-22-105746_lemmynsfw-changes/down.sql b/migrations/2024-01-22-105746_lemmynsfw-changes/down.sql new file mode 100644 index 000000000..1f39df53c --- /dev/null +++ b/migrations/2024-01-22-105746_lemmynsfw-changes/down.sql @@ -0,0 +1,6 @@ +ALTER TABLE site + DROP COLUMN content_warning; + +ALTER TABLE local_site + DROP COLUMN default_post_listing_mode; + diff --git a/migrations/2024-01-22-105746_lemmynsfw-changes/up.sql b/migrations/2024-01-22-105746_lemmynsfw-changes/up.sql new file mode 100644 index 000000000..c7c7d58a8 --- /dev/null +++ b/migrations/2024-01-22-105746_lemmynsfw-changes/up.sql @@ -0,0 +1,6 @@ +ALTER TABLE site + ADD COLUMN content_warning text; + +ALTER TABLE local_site + ADD COLUMN default_post_listing_mode post_listing_mode_enum NOT NULL DEFAULT 'List'; + From ffcf415cacadf3f0084b24aaab72ccbc6b72d4f8 Mon Sep 17 00:00:00 2001 From: Nutomic Date: Fri, 16 Feb 2024 14:50:06 +0100 Subject: [PATCH 07/12] Dont log db url on connection error (fixes #4453) (#4456) * Dont log db url on connection error (fixes #4453) * remove format --- crates/db_schema/src/utils.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 5ec6c7523..be551a160 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -369,8 +369,7 @@ pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); fn run_migrations(db_url: &str) -> Result<(), LemmyError> { // Needs to be a sync connection - let mut conn = - PgConnection::establish(db_url).with_context(|| format!("Error connecting to {db_url}"))?; + let mut conn = PgConnection::establish(db_url).with_context(|| "Error connecting to database")?; info!("Running Database migrations (This may take a long time)..."); conn From 5d551e6da5329b196e103aa97e525cea579c3683 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 16 Feb 2024 09:36:46 -0500 Subject: [PATCH 08/12] Adding an instance-level default_sort_type (#4454) * Adding an instance-level default_sort_type - Fixes #3796 * Fixing comment. * Put user sort before site sort. --- crates/api_common/src/site.rs | 3 ++ crates/api_crud/src/site/create.rs | 16 ++++++++- crates/api_crud/src/site/update.rs | 15 +++++++- crates/apub/src/api/list_comments.rs | 3 +- crates/apub/src/api/list_posts.rs | 15 +++++--- crates/apub/src/api/mod.rs | 36 +++++++++++++++---- crates/db_schema/src/schema.rs | 2 ++ crates/db_schema/src/source/local_site.rs | 7 +++- .../down.sql | 3 ++ .../up.sql | 3 ++ 10 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 migrations/2024-02-15-171358_default_instance_sort_type/down.sql create mode 100644 migrations/2024-02-15-171358_default_instance_sort_type/up.sql diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index b92dd40ce..912fbd343 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -162,6 +162,7 @@ pub struct CreateSite { pub private_instance: Option, pub default_theme: Option, pub default_post_listing_type: Option, + pub default_sort_type: Option, pub legal_information: Option, pub application_email_admins: Option, pub hide_modlog_mod_names: Option, @@ -221,6 +222,8 @@ pub struct EditSite { /// The default theme. Usually "browser" pub default_theme: Option, pub default_post_listing_type: Option, + /// The default sort, usually "active" + pub default_sort_type: Option, /// An optional page of legal information pub legal_information: Option, /// Whether to email admins when receiving a new application. diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 88e91a694..b5441bffe 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -93,6 +93,7 @@ pub async fn create_site( private_instance: data.private_instance, default_theme: data.default_theme.clone(), default_post_listing_type: data.default_post_listing_type, + default_sort_type: data.default_sort_type, legal_information: diesel_option_overwrite(data.legal_information.clone()), application_email_admins: data.application_email_admins, hide_modlog_mod_names: data.hide_modlog_mod_names, @@ -192,7 +193,7 @@ mod tests { use crate::site::create::validate_create_payload; use lemmy_api_common::site::CreateSite; - use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode}; + use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode, SortType}; use lemmy_utils::error::LemmyErrorType; #[test] @@ -214,6 +215,7 @@ mod tests { None::, None::, None::, + None::, None::, None::, None::, @@ -237,6 +239,7 @@ mod tests { None::, None::, None::, + None::, None::, None::, None::, @@ -260,6 +263,7 @@ mod tests { None::, None::, None::, + None::, Some(String::from("(zeta|alpha)")), None::, None::, @@ -283,6 +287,7 @@ mod tests { None::, None::, Some(ListingType::Subscribed), + None::, None::, None::, None::, @@ -306,6 +311,7 @@ mod tests { None::, None::, None::, + None::, None::, Some(true), Some(true), @@ -329,6 +335,7 @@ mod tests { None::, None::, None::, + None::, None::, None::, Some(true), @@ -352,6 +359,7 @@ mod tests { None::, None::, None::, + None::, None::, None::, None::, @@ -409,6 +417,7 @@ mod tests { None::, None::, None::, + None::, None::, None::, None::, @@ -431,6 +440,7 @@ mod tests { Some(String::new()), Some(String::new()), Some(ListingType::All), + Some(SortType::Active), Some(String::new()), Some(false), Some(true), @@ -453,6 +463,7 @@ mod tests { None::, None::, None::, + None::, Some(String::new()), None::, None::, @@ -475,6 +486,7 @@ mod tests { None::, None::, None::, + None::, None::, None::, None::, @@ -524,6 +536,7 @@ mod tests { site_description: Option, site_sidebar: Option, site_listing_type: Option, + site_sort_type: Option, site_slur_filter_regex: Option, site_is_private: Option, site_is_federated: Option, @@ -544,6 +557,7 @@ mod tests { private_instance: site_is_private, default_theme: None, default_post_listing_type: site_listing_type, + default_sort_type: site_sort_type, legal_information: None, application_email_admins: None, hide_modlog_mod_names: None, diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 46bd1e49f..17e81937e 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -92,6 +92,7 @@ pub async fn update_site( private_instance: data.private_instance, default_theme: data.default_theme.clone(), default_post_listing_type: data.default_post_listing_type, + default_sort_type: data.default_sort_type, legal_information: diesel_option_overwrite(data.legal_information.clone()), application_email_admins: data.application_email_admins, hide_modlog_mod_names: data.hide_modlog_mod_names, @@ -227,7 +228,7 @@ mod tests { use crate::site::update::validate_update_payload; use lemmy_api_common::site::EditSite; - use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode}; + use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode, SortType}; use lemmy_utils::error::LemmyErrorType; #[test] @@ -248,6 +249,7 @@ mod tests { None::, None::, None::, + None::, None::, None::, None::, @@ -270,6 +272,7 @@ mod tests { None::, None::, None::, + None::, Some(String::from("(zeta|alpha)")), None::, None::, @@ -292,6 +295,7 @@ mod tests { None::, None::, Some(ListingType::Subscribed), + None::, None::, None::, None::, @@ -314,6 +318,7 @@ mod tests { None::, None::, None::, + None::, None::, Some(true), Some(true), @@ -336,6 +341,7 @@ mod tests { None::, None::, None::, + None::, None::, None::, Some(true), @@ -358,6 +364,7 @@ mod tests { None::, None::, None::, + None::, None::, None::, None::, @@ -411,6 +418,7 @@ mod tests { None::, None::, None::, + None::, None::, None::, None::, @@ -432,6 +440,7 @@ mod tests { Some(String::new()), Some(String::new()), Some(ListingType::All), + Some(SortType::Active), Some(String::new()), Some(false), Some(true), @@ -453,6 +462,7 @@ mod tests { None::, None::, None::, + None::, Some(String::new()), None::, None::, @@ -474,6 +484,7 @@ mod tests { None::, None::, None::, + None::, None::, None::, None::, @@ -521,6 +532,7 @@ mod tests { site_description: Option, site_sidebar: Option, site_listing_type: Option, + site_sort_type: Option, site_slur_filter_regex: Option, site_is_private: Option, site_is_federated: Option, @@ -541,6 +553,7 @@ mod tests { private_instance: site_is_private, default_theme: None, default_post_listing_type: site_listing_type, + default_sort_type: site_sort_type, legal_information: None, application_email_admins: None, hide_modlog_mod_names: None, diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index 7d1de019e..3ae85cdcc 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -48,9 +48,10 @@ pub async fn list_comments( let listing_type = Some(listing_type_with_default( data.type_, + local_user_view.as_ref().map(|u| &u.local_user), &local_site, community_id, - )?); + )); // If a parent_id is given, fetch the comment to get the path let parent_path = if let Some(parent_id) = parent_id { diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index d4ed566c4..5285acaa7 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -1,5 +1,5 @@ use crate::{ - api::listing_type_with_default, + api::{listing_type_with_default, sort_type_with_default}, fetcher::resolve_actor_identifier, objects::community::ApubCommunity, }; @@ -27,8 +27,6 @@ pub async fn list_posts( check_private_instance(&local_user_view, &local_site.local_site)?; - let sort = data.sort; - let page = data.page; let limit = data.limit; let community_id = if let Some(name) = &data.community_name { @@ -45,11 +43,20 @@ pub async fn list_posts( return Err(LemmyError::from(LemmyErrorType::ContradictingFilters)); } + let local_user_ref = local_user_view.as_ref().map(|u| &u.local_user); let listing_type = Some(listing_type_with_default( data.type_, + local_user_ref, &local_site.local_site, community_id, - )?); + )); + + let sort = Some(sort_type_with_default( + data.sort, + local_user_ref, + &local_site.local_site, + )); + // parse pagination token let page_after = if let Some(pa) = &data.page_cursor { Some(pa.read(&mut context.pool()).await?) diff --git a/crates/apub/src/api/mod.rs b/crates/apub/src/api/mod.rs index 59586e477..dab2ace06 100644 --- a/crates/apub/src/api/mod.rs +++ b/crates/apub/src/api/mod.rs @@ -1,5 +1,9 @@ -use lemmy_db_schema::{newtypes::CommunityId, source::local_site::LocalSite, ListingType}; -use lemmy_utils::error::LemmyError; +use lemmy_db_schema::{ + newtypes::CommunityId, + source::{local_site::LocalSite, local_user::LocalUser}, + ListingType, + SortType, +}; pub mod list_comments; pub mod list_posts; @@ -12,15 +16,33 @@ pub mod user_settings_backup; /// Returns default listing type, depending if the query is for frontpage or community. fn listing_type_with_default( type_: Option, + local_user: Option<&LocalUser>, local_site: &LocalSite, community_id: Option, -) -> Result { +) -> ListingType { // On frontpage use listing type from param or admin configured default - let listing_type = if community_id.is_none() { - type_.unwrap_or(local_site.default_post_listing_type) + if community_id.is_none() { + type_.unwrap_or( + local_user + .map(|u| u.default_listing_type) + .unwrap_or(local_site.default_post_listing_type), + ) } else { // inside of community show everything ListingType::All - }; - Ok(listing_type) + } +} + +/// Returns a default instance-level sort type, if none is given by the user. +/// Order is type, local user default, then site default. +fn sort_type_with_default( + type_: Option, + local_user: Option<&LocalUser>, + local_site: &LocalSite, +) -> SortType { + type_.unwrap_or( + local_user + .map(|u| u.default_sort_type) + .unwrap_or(local_site.default_sort_type), + ) } diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index de54c379c..19f9183a5 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -354,6 +354,7 @@ diesel::table! { use super::sql_types::ListingTypeEnum; use super::sql_types::RegistrationModeEnum; use super::sql_types::PostListingModeEnum; + use super::sql_types::SortTypeEnum; local_site (id) { id -> Int4, @@ -382,6 +383,7 @@ diesel::table! { reports_email_admins -> Bool, federation_signed_fetch -> Bool, default_post_listing_mode -> PostListingModeEnum, + default_sort_type -> SortTypeEnum, } } diff --git a/crates/db_schema/src/source/local_site.rs b/crates/db_schema/src/source/local_site.rs index ea3dbc179..05583c065 100644 --- a/crates/db_schema/src/source/local_site.rs +++ b/crates/db_schema/src/source/local_site.rs @@ -5,6 +5,7 @@ use crate::{ ListingType, PostListingMode, RegistrationMode, + SortType, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -65,8 +66,10 @@ pub struct LocalSite { /// Whether to sign outgoing Activitypub fetches with private key of local instance. Some /// Fediverse instances and platforms require this. pub federation_signed_fetch: bool, - /// Default value for [LocalUser.post_listing_mode] + /// Default value for [LocalSite.post_listing_mode] pub default_post_listing_mode: PostListingMode, + /// Default value for [LocalUser.post_listing_mode] + pub default_sort_type: SortType, } #[derive(Clone, TypedBuilder)] @@ -97,6 +100,7 @@ pub struct LocalSiteInsertForm { pub reports_email_admins: Option, pub federation_signed_fetch: Option, pub default_post_listing_mode: Option, + pub default_sort_type: Option, } #[derive(Clone, Default)] @@ -125,4 +129,5 @@ pub struct LocalSiteUpdateForm { pub updated: Option>>, pub federation_signed_fetch: Option, pub default_post_listing_mode: Option, + pub default_sort_type: Option, } diff --git a/migrations/2024-02-15-171358_default_instance_sort_type/down.sql b/migrations/2024-02-15-171358_default_instance_sort_type/down.sql new file mode 100644 index 000000000..8ba51592b --- /dev/null +++ b/migrations/2024-02-15-171358_default_instance_sort_type/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_site + DROP COLUMN default_sort_type; + diff --git a/migrations/2024-02-15-171358_default_instance_sort_type/up.sql b/migrations/2024-02-15-171358_default_instance_sort_type/up.sql new file mode 100644 index 000000000..f2bcddcff --- /dev/null +++ b/migrations/2024-02-15-171358_default_instance_sort_type/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_site + ADD COLUMN default_sort_type sort_type_enum DEFAULT 'Active' NOT NULL; + From 39345466da408e5893c2bcef07f6d10d2941c5fa Mon Sep 17 00:00:00 2001 From: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com> Date: Sun, 18 Feb 2024 14:09:46 +0000 Subject: [PATCH 09/12] Make it so the signed in user can see if they're banned from a community (#4458) * Make it so the signed in user can see if they're banned from a community * Use more appropriate field name --------- Co-authored-by: SleeplessOne1917 --- crates/db_views_actor/src/community_view.rs | 9 +++++++++ crates/db_views_actor/src/structs.rs | 1 + 2 files changed, 10 insertions(+) diff --git a/crates/db_views_actor/src/community_view.rs b/crates/db_views_actor/src/community_view.rs index 828738c27..c1cb6eee1 100644 --- a/crates/db_views_actor/src/community_view.rs +++ b/crates/db_views_actor/src/community_view.rs @@ -17,6 +17,7 @@ use lemmy_db_schema::{ community_aggregates, community_block, community_follower, + community_person_ban, instance_block, local_user, }, @@ -58,6 +59,13 @@ fn queries<'a>() -> Queries< .and(community_block::person_id.eq(person_id_join)), ), ) + .left_join( + community_person_ban::table.on( + community::id + .eq(community_person_ban::community_id) + .and(community_person_ban::person_id.eq(person_id_join)), + ), + ) }; let selection = ( @@ -65,6 +73,7 @@ fn queries<'a>() -> Queries< CommunityFollower::select_subscribed_type(), community_block::community_id.nullable().is_not_null(), community_aggregates::all_columns, + community_person_ban::person_id.nullable().is_not_null(), ); let not_removed_or_deleted = community::removed diff --git a/crates/db_views_actor/src/structs.rs b/crates/db_views_actor/src/structs.rs index 46817be78..f25662f7b 100644 --- a/crates/db_views_actor/src/structs.rs +++ b/crates/db_views_actor/src/structs.rs @@ -80,6 +80,7 @@ pub struct CommunityView { pub subscribed: SubscribedType, pub blocked: bool, pub counts: CommunityAggregates, + pub banned_from_community: bool, } #[derive(Debug, Serialize, Deserialize, Clone)] From ae62ef2b7efaa9279820069f28224881cc1e2523 Mon Sep 17 00:00:00 2001 From: dullbananas Date: Sun, 18 Feb 2024 07:12:12 -0700 Subject: [PATCH 10/12] Ignore expired bans in CommentReportView::read, just like in CommentReportQuery::list (#4457) * Update comment_report_view.rs * Update comment_report_view.rs * Update comment_report_view.rs * Update comment_report_view.rs --- crates/db_views/src/comment_report_view.rs | 62 +++++++++------------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index 37ed6825c..05d95a756 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -52,41 +52,6 @@ fn queries<'a>() -> Queries< aliases::person2 .on(comment_report::resolver_id.eq(aliases::person2.field(person::id).nullable())), ) - }; - - let selection = ( - comment_report::all_columns, - comment::all_columns, - post::all_columns, - community::all_columns, - person::all_columns, - aliases::person1.fields(person::all_columns), - comment_aggregates::all_columns, - community_person_ban::community_id.nullable().is_not_null(), - comment_like::score.nullable(), - aliases::person2.fields(person::all_columns).nullable(), - ); - - let read = move |mut conn: DbConn<'a>, (report_id, my_person_id): (CommentReportId, PersonId)| async move { - all_joins( - comment_report::table.find(report_id).into_boxed(), - my_person_id, - ) - .left_join( - community_person_ban::table.on( - community::id - .eq(community_person_ban::community_id) - .and(community_person_ban::person_id.eq(comment::creator_id)), - ), - ) - .select(selection) - .first::(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, - (options, user): (CommentReportQuery, &'a LocalUserView)| async move { - let mut query = all_joins(comment_report::table.into_boxed(), user.person.id) .left_join( community_person_ban::table.on( community::id @@ -99,7 +64,32 @@ fn queries<'a>() -> Queries< ), ), ) - .select(selection); + .select(( + comment_report::all_columns, + comment::all_columns, + post::all_columns, + community::all_columns, + person::all_columns, + aliases::person1.fields(person::all_columns), + comment_aggregates::all_columns, + community_person_ban::community_id.nullable().is_not_null(), + comment_like::score.nullable(), + aliases::person2.fields(person::all_columns).nullable(), + )) + }; + + let read = move |mut conn: DbConn<'a>, (report_id, my_person_id): (CommentReportId, PersonId)| async move { + all_joins( + comment_report::table.find(report_id).into_boxed(), + my_person_id, + ) + .first::(&mut conn) + .await + }; + + let list = move |mut conn: DbConn<'a>, + (options, user): (CommentReportQuery, &'a LocalUserView)| async move { + let mut query = all_joins(comment_report::table.into_boxed(), user.person.id); if let Some(community_id) = options.community_id { query = query.filter(post::community_id.eq(community_id)); From d79502dff3401e23dfb538edc73fbb3cbfe4d8d9 Mon Sep 17 00:00:00 2001 From: dullbananas Date: Sun, 18 Feb 2024 07:12:56 -0700 Subject: [PATCH 11/12] Escape backslashes in fuzzy_search (#4462) * Escape backslashes in fuzzy_search * Update utils.rs --- crates/db_schema/src/utils.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index be551a160..213e0015e 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -236,7 +236,11 @@ impl LimitDsl for Commented { } pub fn fuzzy_search(q: &str) -> String { - let replaced = q.replace('%', "\\%").replace('_', "\\_").replace(' ', "%"); + let replaced = q + .replace('\\', "\\\\") + .replace('%', "\\%") + .replace('_', "\\_") + .replace(' ', "%"); format!("%{replaced}%") } From f56b84615c78a0ca9942e7a82c8d7ce4255ec5cb Mon Sep 17 00:00:00 2001 From: dullbananas Date: Mon, 19 Feb 2024 10:41:28 -0700 Subject: [PATCH 12/12] Move DbUrl trait impls to newtypes.rs (#4463) * Move DbUrl trait impls to newtypes.rs * Update utils.rs --- crates/db_schema/src/newtypes.rs | 37 ++++++++++++++++++++++++++++++++ crates/db_schema/src/utils.rs | 32 +-------------------------- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 5e5970e91..96fc23ac6 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -6,6 +6,14 @@ use activitypub_federation::{ traits::Object, }; #[cfg(feature = "full")] +use diesel::{ + backend::Backend, + deserialize::FromSql, + pg::Pg, + serialize::{Output, ToSql}, + sql_types::Text, +}; +#[cfg(feature = "full")] use diesel_ltree::Ltree; use serde::{Deserialize, Serialize}; use std::{ @@ -239,6 +247,35 @@ impl TS for DbUrl { } } +#[cfg(feature = "full")] +impl ToSql for DbUrl { + fn to_sql(&self, out: &mut Output) -> diesel::serialize::Result { + >::to_sql(&self.0.to_string(), &mut out.reborrow()) + } +} + +#[cfg(feature = "full")] +impl FromSql for DbUrl +where + String: FromSql, +{ + fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { + let str = String::from_sql(value)?; + Ok(DbUrl(Box::new(Url::parse(&str)?))) + } +} + +#[cfg(feature = "full")] +impl From> for DbUrl +where + Kind: Object + Send + 'static, + for<'de2> ::Kind: serde::Deserialize<'de2>, +{ + fn from(id: ObjectId) -> Self { + DbUrl(Box::new(id.into())) + } +} + impl InstanceId { pub fn inner(self) -> i32 { self.0 diff --git a/crates/db_schema/src/utils.rs b/crates/db_schema/src/utils.rs index 213e0015e..6afc850b5 100644 --- a/crates/db_schema/src/utils.rs +++ b/crates/db_schema/src/utils.rs @@ -5,20 +5,16 @@ use crate::{ CommentSortType, SortType, }; -use activitypub_federation::{fetch::object_id::ObjectId, traits::Object}; use anyhow::Context; use chrono::{DateTime, Utc}; use deadpool::Runtime; use diesel::{ - backend::Backend, - deserialize::FromSql, helper_types::AsExprOf, pg::Pg, query_builder::{Query, QueryFragment}, query_dsl::methods::LimitDsl, result::{ConnectionError, ConnectionResult, Error as DieselError, Error::QueryBuilderError}, - serialize::{Output, ToSql}, - sql_types::{self, Text, Timestamptz}, + sql_types::{self, Timestamptz}, IntoSql, PgConnection, }; @@ -475,32 +471,6 @@ pub mod functions { pub const DELETED_REPLACEMENT_TEXT: &str = "*Permanently Deleted*"; -impl ToSql for DbUrl { - fn to_sql(&self, out: &mut Output) -> diesel::serialize::Result { - >::to_sql(&self.0.to_string(), &mut out.reborrow()) - } -} - -impl FromSql for DbUrl -where - String: FromSql, -{ - fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { - let str = String::from_sql(value)?; - Ok(DbUrl(Box::new(Url::parse(&str)?))) - } -} - -impl From> for DbUrl -where - Kind: Object + Send + 'static, - for<'de2> ::Kind: serde::Deserialize<'de2>, -{ - fn from(id: ObjectId) -> Self { - DbUrl(Box::new(id.into())) - } -} - pub fn now() -> AsExprOf { // https://github.com/diesel-rs/diesel/issues/1514 diesel::dsl::now.into_sql::()