Adding local site settings to reject federated upvotes or downvotes. (#5038)

* Adding local site settings to reject federated upvotes or downvotes.

- Should help defend against downvote spamming instances.
- Fixes #4086

* Adding new vote mode types.

* Simpler activitypub vote check.

* Adding undo vote for failed vote mode check.

* Update crates/api_common/src/utils.rs

---------

Co-authored-by: Nutomic <me@nutomic.com>
This commit is contained in:
Dessalines 2024-10-02 06:55:37 -04:00 committed by GitHub
parent e3edc317be
commit ffb94fde85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 217 additions and 39 deletions

View file

@ -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<Json<CommentResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let comment_id = data.comment_id;
let mut recipient_ids = Vec::<LocalUserId>::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,

View file

@ -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<Json<PostResponse>> {
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(

View file

@ -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<String>,
pub icon: Option<String>,
pub banner: Option<String>,
pub enable_downvotes: Option<bool>,
pub enable_nsfw: Option<bool>,
pub community_creation_admin_only: Option<bool>,
pub require_email_verification: Option<bool>,
@ -208,6 +208,10 @@ pub struct CreateSite {
pub registration_mode: Option<RegistrationMode>,
pub oauth_registration: Option<bool>,
pub content_warning: Option<String>,
pub post_upvotes: Option<FederationMode>,
pub post_downvotes: Option<FederationMode>,
pub comment_upvotes: Option<FederationMode>,
pub comment_downvotes: Option<FederationMode>,
}
#[skip_serializing_none]
@ -224,8 +228,6 @@ pub struct EditSite {
pub icon: Option<String>,
/// A url for your site's banner.
pub banner: Option<String>,
/// Whether to enable downvotes.
pub enable_downvotes: Option<bool>,
/// Whether to enable NSFW.
pub enable_nsfw: Option<bool>,
/// Limits community creation to admins only.
@ -291,13 +293,21 @@ pub struct EditSite {
/// A list of blocked URLs
pub blocked_urls: Option<Vec<String>>,
pub registration_mode: Option<RegistrationMode>,
/// Whether or not external auth methods can auto-register users.
pub oauth_registration: Option<bool>,
/// Whether to email admins for new reports.
pub reports_email_admins: Option<bool>,
/// 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<String>,
/// Whether or not external auth methods can auto-register users.
pub oauth_registration: Option<bool>,
/// What kind of post upvotes your site allows.
pub post_upvotes: Option<FederationMode>,
/// What kind of post downvotes your site allows.
pub post_downvotes: Option<FederationMode>,
/// What kind of comment upvotes your site allows.
pub comment_upvotes: Option<FederationMode>,
/// What kind of comment downvotes your site allows.
pub comment_downvotes: Option<FederationMode>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]

View file

@ -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

View file

@ -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()
};

View file

@ -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()
};

View file

@ -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,

View file

@ -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]

View file

@ -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<Text>,
@ -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,
}
}

View file

@ -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<bool>,
#[new(default)]
pub enable_downvotes: Option<bool>,
#[new(default)]
pub community_creation_admin_only: Option<bool>,
#[new(default)]
pub require_email_verification: Option<bool>,
@ -114,8 +119,6 @@ pub struct LocalSiteInsertForm {
#[new(default)]
pub registration_mode: Option<RegistrationMode>,
#[new(default)]
pub oauth_registration: Option<bool>,
#[new(default)]
pub reports_email_admins: Option<bool>,
#[new(default)]
pub federation_signed_fetch: Option<bool>,
@ -125,6 +128,16 @@ pub struct LocalSiteInsertForm {
pub default_post_sort_type: Option<PostSortType>,
#[new(default)]
pub default_comment_sort_type: Option<CommentSortType>,
#[new(default)]
pub oauth_registration: Option<bool>,
#[new(default)]
pub post_upvotes: Option<FederationMode>,
#[new(default)]
pub post_downvotes: Option<FederationMode>,
#[new(default)]
pub comment_upvotes: Option<FederationMode>,
#[new(default)]
pub comment_downvotes: Option<FederationMode>,
}
#[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<bool>,
pub enable_downvotes: Option<bool>,
pub community_creation_admin_only: Option<bool>,
pub require_email_verification: Option<bool>,
pub application_question: Option<Option<String>>,
@ -148,11 +160,15 @@ pub struct LocalSiteUpdateForm {
pub captcha_enabled: Option<bool>,
pub captcha_difficulty: Option<String>,
pub registration_mode: Option<RegistrationMode>,
pub oauth_registration: Option<bool>,
pub reports_email_admins: Option<bool>,
pub updated: Option<Option<DateTime<Utc>>>,
pub federation_signed_fetch: Option<bool>,
pub default_post_listing_mode: Option<PostListingMode>,
pub default_post_sort_type: Option<PostSortType>,
pub default_comment_sort_type: Option<CommentSortType>,
pub oauth_registration: Option<bool>,
pub post_upvotes: Option<FederationMode>,
pub post_downvotes: Option<FederationMode>,
pub comment_upvotes: Option<FederationMode>,
pub comment_downvotes: Option<FederationMode>,
}

View file

@ -46,7 +46,7 @@ pub enum LemmyErrorType {
PersonIsBlocked,
CommunityIsBlocked,
InstanceIsBlocked,
DownvotesAreDisabled,
VoteNotAllowed,
InstanceIsPrivate,
/// Password must be between 10 and 60 characters
InvalidPassword,

View file

@ -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;

View file

@ -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;