From 86b44c2a4dcde8f2dd2db33af56824b7a0ef4119 Mon Sep 17 00:00:00 2001 From: Nutomic Date: Fri, 16 Feb 2024 13:24:35 +0100 Subject: [PATCH] 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'; +