diff --git a/crates/api/src/comment/like.rs b/crates/api/src/comment/like.rs index 81d5516c1..749b90426 100644 --- a/crates/api/src/comment/like.rs +++ b/crates/api/src/comment/like.rs @@ -5,7 +5,7 @@ use lemmy_api_common::{ comment::{CommentResponse, CreateCommentLike}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_bot_account, check_community_user_action, check_downvotes_enabled}, + utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem}, }; use lemmy_db_schema::{ newtypes::LocalUserId, @@ -27,14 +27,20 @@ pub async fn like_comment( local_user_view: LocalUserView, ) -> LemmyResult> { let local_site = LocalSite::read(&mut context.pool()).await?; + let comment_id = data.comment_id; let mut recipient_ids = Vec::::new(); - // Don't do a downvote if site has downvotes disabled - check_downvotes_enabled(data.score, &local_site)?; + check_local_vote_mode( + data.score, + VoteItem::Comment(comment_id), + &local_site, + local_user_view.person.id, + &mut context.pool(), + ) + .await?; check_bot_account(&local_user_view.person)?; - let comment_id = data.comment_id; let orig_comment = CommentView::read( &mut context.pool(), comment_id, diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index 967b22a30..c81d9630a 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -8,8 +8,9 @@ use lemmy_api_common::{ utils::{ check_bot_account, check_community_user_action, - check_downvotes_enabled, + check_local_vote_mode, mark_post_as_read, + VoteItem, }, }; use lemmy_db_schema::{ @@ -31,13 +32,19 @@ pub async fn like_post( local_user_view: LocalUserView, ) -> LemmyResult> { let local_site = LocalSite::read(&mut context.pool()).await?; + let post_id = data.post_id; - // Don't do a downvote if site has downvotes disabled - check_downvotes_enabled(data.score, &local_site)?; + check_local_vote_mode( + data.score, + VoteItem::Post(post_id), + &local_site, + local_user_view.person.id, + &mut context.pool(), + ) + .await?; check_bot_account(&local_user_view.person)?; // Check for a community ban - let post_id = data.post_id; let post = Post::read(&mut context.pool(), post_id).await?; check_community_user_action( diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index d82303327..8316b30ee 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -21,6 +21,7 @@ use lemmy_db_schema::{ tagline::Tagline, }, CommentSortType, + FederationMode, ListingType, ModlogActionType, PostListingMode, @@ -170,7 +171,6 @@ pub struct CreateSite { pub description: Option, pub icon: Option, pub banner: Option, - pub enable_downvotes: Option, pub enable_nsfw: Option, pub community_creation_admin_only: Option, pub require_email_verification: Option, @@ -208,6 +208,10 @@ pub struct CreateSite { pub registration_mode: Option, pub oauth_registration: Option, pub content_warning: Option, + pub post_upvotes: Option, + pub post_downvotes: Option, + pub comment_upvotes: Option, + pub comment_downvotes: Option, } #[skip_serializing_none] @@ -224,8 +228,6 @@ pub struct EditSite { pub icon: Option, /// A url for your site's banner. pub banner: Option, - /// Whether to enable downvotes. - pub enable_downvotes: Option, /// Whether to enable NSFW. pub enable_nsfw: Option, /// Limits community creation to admins only. @@ -291,13 +293,21 @@ pub struct EditSite { /// A list of blocked URLs pub blocked_urls: Option>, pub registration_mode: Option, - /// Whether or not external auth methods can auto-register users. - pub oauth_registration: 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, + /// Whether or not external auth methods can auto-register users. + pub oauth_registration: Option, + /// What kind of post upvotes your site allows. + pub post_upvotes: Option, + /// What kind of post downvotes your site allows. + pub post_downvotes: Option, + /// What kind of comment upvotes your site allows. + pub comment_upvotes: Option, + /// What kind of comment downvotes your site allows. + pub comment_downvotes: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 2ba267420..b55cd92bb 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::{ aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId}, source::{ - comment::{Comment, CommentUpdateForm}, + comment::{Comment, CommentLike, CommentUpdateForm}, community::{Community, CommunityModerator, CommunityUpdateForm}, community_block::CommunityBlock, email_verification::{EmailVerification, EmailVerificationForm}, @@ -28,12 +28,13 @@ use lemmy_db_schema::{ password_reset_request::PasswordResetRequest, person::{Person, PersonUpdateForm}, person_block::PersonBlock, - post::{Post, PostRead}, + post::{Post, PostLike, PostRead}, registration_application::RegistrationApplication, site::Site, }, - traits::Crud, + traits::{Crud, Likeable}, utils::DbPool, + FederationMode, RegistrationMode, }; use lemmy_db_views::{ @@ -297,13 +298,36 @@ pub async fn check_person_instance_community_block( Ok(()) } +/// A vote item type used to check the vote mode. +pub enum VoteItem { + Post(PostId), + Comment(CommentId), +} + #[tracing::instrument(skip_all)] -pub fn check_downvotes_enabled(score: i16, local_site: &LocalSite) -> LemmyResult<()> { - if score == -1 && !local_site.enable_downvotes { - Err(LemmyErrorType::DownvotesAreDisabled)? - } else { - Ok(()) +pub async fn check_local_vote_mode( + score: i16, + vote_item: VoteItem, + local_site: &LocalSite, + person_id: PersonId, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + let (downvote_setting, upvote_setting) = match vote_item { + VoteItem::Post(_) => (local_site.post_downvotes, local_site.post_upvotes), + VoteItem::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes), + }; + + let downvote_fail = score == -1 && downvote_setting == FederationMode::Disable; + let upvote_fail = score == 1 && upvote_setting == FederationMode::Disable; + + // Undo previous vote for item if new vote fails + if downvote_fail || upvote_fail { + match vote_item { + VoteItem::Post(post_id) => PostLike::remove(pool, person_id, post_id).await?, + VoteItem::Comment(comment_id) => CommentLike::remove(pool, person_id, comment_id).await?, + }; } + Ok(()) } /// Dont allow bots to do certain actions, like voting diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 9b0439da8..07efa72b9 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -90,7 +90,6 @@ pub async fn create_site( let local_site_form = LocalSiteUpdateForm { // Set the site setup to true site_setup: Some(true), - enable_downvotes: data.enable_downvotes, registration_mode: data.registration_mode, community_creation_admin_only: data.community_creation_admin_only, require_email_verification: data.require_email_verification, @@ -110,6 +109,10 @@ pub async fn create_site( captcha_enabled: data.captcha_enabled, captcha_difficulty: data.captcha_difficulty.clone(), default_post_listing_mode: data.default_post_listing_mode, + post_upvotes: data.post_upvotes, + post_downvotes: data.post_downvotes, + comment_upvotes: data.comment_upvotes, + comment_downvotes: data.comment_downvotes, ..Default::default() }; diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 495a5cc98..cce428cc1 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -99,7 +99,6 @@ pub async fn update_site( .ok(); let local_site_form = LocalSiteUpdateForm { - enable_downvotes: data.enable_downvotes, registration_mode: data.registration_mode, community_creation_admin_only: data.community_creation_admin_only, require_email_verification: data.require_email_verification, @@ -121,6 +120,10 @@ pub async fn update_site( reports_email_admins: data.reports_email_admins, default_post_listing_mode: data.default_post_listing_mode, oauth_registration: data.oauth_registration, + post_upvotes: data.post_upvotes, + post_downvotes: data.post_downvotes, + comment_upvotes: data.comment_upvotes, + comment_downvotes: data.comment_downvotes, ..Default::default() }; diff --git a/crates/apub/src/activities/voting/vote.rs b/crates/apub/src/activities/voting/vote.rs index 324c8b300..1cdc81952 100644 --- a/crates/apub/src/activities/voting/vote.rs +++ b/crates/apub/src/activities/voting/vote.rs @@ -18,7 +18,7 @@ use activitypub_federation::{ traits::{ActivityHandler, Actor}, }; use lemmy_api_common::{context::LemmyContext, utils::check_bot_account}; -use lemmy_db_schema::source::local_site::LocalSite; +use lemmy_db_schema::{source::local_site::LocalSite, FederationMode}; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; @@ -68,12 +68,22 @@ impl ActivityHandler for Vote { check_bot_account(&actor.0)?; - let enable_downvotes = LocalSite::read(&mut context.pool()) + // Check for enabled federation votes + let local_site = LocalSite::read(&mut context.pool()) .await - .map(|l| l.enable_downvotes) - .unwrap_or(true); - if self.kind == VoteType::Dislike && !enable_downvotes { - // If this is a downvote but downvotes are ignored, only undo any existing vote + .unwrap_or_default(); + + let (downvote_setting, upvote_setting) = match object { + PostOrComment::Post(_) => (local_site.post_downvotes, local_site.post_upvotes), + PostOrComment::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes), + }; + + // Don't allow dislikes for either disabled, or local only votes + let downvote_fail = self.kind == VoteType::Dislike && downvote_setting != FederationMode::All; + let upvote_fail = self.kind == VoteType::Like && upvote_setting != FederationMode::All; + + if downvote_fail || upvote_fail { + // If this is a rejection, undo the vote match object { PostOrComment::Post(p) => undo_vote_post(actor, &p, context).await, PostOrComment::Comment(c) => undo_vote_comment(actor, &c, context).await, diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 963ac63d7..dbadaaf95 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -251,6 +251,27 @@ pub enum CommunityVisibility { LocalOnly, } +#[derive( + EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash, +)] +#[cfg_attr(feature = "full", derive(DbEnum, TS))] +#[cfg_attr( + feature = "full", + ExistingTypePath = "crate::schema::sql_types::FederationModeEnum" +)] +#[cfg_attr(feature = "full", DbValueStyle = "verbatim")] +#[cfg_attr(feature = "full", ts(export))] +/// The federation mode for an item +pub enum FederationMode { + #[default] + /// Allows all + All, + /// Allows only local + Local, + /// Disables + Disable, +} + /// Wrapper for assert_eq! macro. Checks that vec matches the given length, and prints the /// vec on failure. #[macro_export] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 9617d9954..5534c4f60 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -13,6 +13,10 @@ pub mod sql_types { #[diesel(postgres_type(name = "community_visibility"))] pub struct CommunityVisibility; + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "federation_mode_enum"))] + pub struct FederationModeEnum; + #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "listing_type_enum"))] pub struct ListingTypeEnum; @@ -368,12 +372,12 @@ diesel::table! { use super::sql_types::PostListingModeEnum; use super::sql_types::PostSortTypeEnum; use super::sql_types::CommentSortTypeEnum; + use super::sql_types::FederationModeEnum; local_site (id) { id -> Int4, site_id -> Int4, site_setup -> Bool, - enable_downvotes -> Bool, community_creation_admin_only -> Bool, require_email_verification -> Bool, application_question -> Nullable, @@ -398,6 +402,10 @@ diesel::table! { default_post_sort_type -> PostSortTypeEnum, default_comment_sort_type -> CommentSortTypeEnum, oauth_registration -> Bool, + post_upvotes -> FederationModeEnum, + post_downvotes -> FederationModeEnum, + comment_upvotes -> FederationModeEnum, + comment_downvotes -> FederationModeEnum, } } diff --git a/crates/db_schema/src/source/local_site.rs b/crates/db_schema/src/source/local_site.rs index 5131ce7ac..5fa57fe3b 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}, CommentSortType, + FederationMode, ListingType, PostListingMode, PostSortType, @@ -27,8 +28,6 @@ pub struct LocalSite { pub site_id: SiteId, /// True if the site is set up. pub site_setup: bool, - /// Whether downvotes are enabled. - pub enable_downvotes: bool, /// Whether only admins can create communities. pub community_creation_admin_only: bool, /// Whether emails are required. @@ -72,6 +71,14 @@ pub struct LocalSite { pub default_comment_sort_type: CommentSortType, /// Whether or not external auth methods can auto-register users. pub oauth_registration: bool, + /// What kind of post upvotes your site allows. + pub post_upvotes: FederationMode, + /// What kind of post downvotes your site allows. + pub post_downvotes: FederationMode, + /// What kind of comment upvotes your site allows. + pub comment_upvotes: FederationMode, + /// What kind of comment downvotes your site allows. + pub comment_downvotes: FederationMode, } #[derive(Clone, derive_new::new)] @@ -82,8 +89,6 @@ pub struct LocalSiteInsertForm { #[new(default)] pub site_setup: Option, #[new(default)] - pub enable_downvotes: Option, - #[new(default)] pub community_creation_admin_only: Option, #[new(default)] pub require_email_verification: Option, @@ -114,8 +119,6 @@ pub struct LocalSiteInsertForm { #[new(default)] pub registration_mode: Option, #[new(default)] - pub oauth_registration: Option, - #[new(default)] pub reports_email_admins: Option, #[new(default)] pub federation_signed_fetch: Option, @@ -125,6 +128,16 @@ pub struct LocalSiteInsertForm { pub default_post_sort_type: Option, #[new(default)] pub default_comment_sort_type: Option, + #[new(default)] + pub oauth_registration: Option, + #[new(default)] + pub post_upvotes: Option, + #[new(default)] + pub post_downvotes: Option, + #[new(default)] + pub comment_upvotes: Option, + #[new(default)] + pub comment_downvotes: Option, } #[derive(Clone, Default)] @@ -132,7 +145,6 @@ pub struct LocalSiteInsertForm { #[cfg_attr(feature = "full", diesel(table_name = local_site))] pub struct LocalSiteUpdateForm { pub site_setup: Option, - pub enable_downvotes: Option, pub community_creation_admin_only: Option, pub require_email_verification: Option, pub application_question: Option>, @@ -148,11 +160,15 @@ pub struct LocalSiteUpdateForm { pub captcha_enabled: Option, pub captcha_difficulty: Option, pub registration_mode: Option, - pub oauth_registration: Option, pub reports_email_admins: Option, pub updated: Option>>, pub federation_signed_fetch: Option, pub default_post_listing_mode: Option, pub default_post_sort_type: Option, pub default_comment_sort_type: Option, + pub oauth_registration: Option, + pub post_upvotes: Option, + pub post_downvotes: Option, + pub comment_upvotes: Option, + pub comment_downvotes: Option, } diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index e03ff2e23..0efabb32c 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -46,7 +46,7 @@ pub enum LemmyErrorType { PersonIsBlocked, CommunityIsBlocked, InstanceIsBlocked, - DownvotesAreDisabled, + VoteNotAllowed, InstanceIsPrivate, /// Password must be between 10 and 60 characters InvalidPassword, diff --git a/migrations/2024-09-20-134838_add_federation_vote_rejection/down.sql b/migrations/2024-09-20-134838_add_federation_vote_rejection/down.sql new file mode 100644 index 000000000..a9181ca4a --- /dev/null +++ b/migrations/2024-09-20-134838_add_federation_vote_rejection/down.sql @@ -0,0 +1,31 @@ +-- Add back the enable_downvotes column +ALTER TABLE local_site + ADD COLUMN enable_downvotes boolean DEFAULT TRUE NOT NULL; + +-- regenerate their values (from post_downvotes alone) +WITH subquery AS ( + SELECT + post_downvotes, + CASE WHEN post_downvotes = 'Disable'::federation_mode_enum THEN + FALSE + ELSE + TRUE + END + FROM + local_site) +UPDATE + local_site +SET + enable_downvotes = subquery.case +FROM + subquery; + +-- Drop the new columns +ALTER TABLE local_site + DROP COLUMN post_upvotes, + DROP COLUMN post_downvotes, + DROP COLUMN comment_upvotes, + DROP COLUMN comment_downvotes; + +DROP TYPE federation_mode_enum; + diff --git a/migrations/2024-09-20-134838_add_federation_vote_rejection/up.sql b/migrations/2024-09-20-134838_add_federation_vote_rejection/up.sql new file mode 100644 index 000000000..a2ee6ad4d --- /dev/null +++ b/migrations/2024-09-20-134838_add_federation_vote_rejection/up.sql @@ -0,0 +1,39 @@ +-- This removes the simple enable_downvotes setting, in favor of an +-- expanded federation mode type for post/comment up/downvotes. +-- Create the federation mode enum +CREATE TYPE federation_mode_enum AS ENUM ( + 'All', + 'Local', + 'Disable' +); + +-- Add the new columns +ALTER TABLE local_site + ADD COLUMN post_upvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL, + ADD COLUMN post_downvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL, + ADD COLUMN comment_upvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL, + ADD COLUMN comment_downvotes federation_mode_enum DEFAULT 'All'::federation_mode_enum NOT NULL; + +-- Copy over the enable_downvotes into the post and comment downvote settings +WITH subquery AS ( + SELECT + enable_downvotes, + CASE WHEN enable_downvotes = TRUE THEN + 'All'::federation_mode_enum + ELSE + 'Disable'::federation_mode_enum + END + FROM + local_site) +UPDATE + local_site +SET + post_downvotes = subquery.case, + comment_downvotes = subquery.case +FROM + subquery; + +-- Drop the enable_downvotes column +ALTER TABLE local_site + DROP COLUMN enable_downvotes; +