Add community setting only_followers_can_vote

This commit is contained in:
Felix Ableitner 2024-01-22 15:14:40 +01:00
parent 5a3a4d9fad
commit d8aa6b3350
15 changed files with 114 additions and 50 deletions

View file

@ -5,16 +5,17 @@ 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_community_user_action, check_vote_permission},
};
use lemmy_db_schema::{
newtypes::LocalUserId,
source::{
comment::{CommentLike, CommentLikeForm},
comment_reply::CommentReply,
community::Community,
local_site::LocalSite,
},
traits::Likeable,
traits::{Crud, Likeable},
};
use lemmy_db_views::structs::{CommentView, LocalUserView};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
@ -30,13 +31,19 @@ pub async fn like_comment(
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_bot_account(&local_user_view.person)?;
let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?;
let community = Community::read(&mut context.pool(), orig_comment.post.community_id).await?;
check_vote_permission(
data.score,
&local_site,
&local_user_view.person,
&community,
&context,
)
.await?;
check_community_user_action(
&local_user_view.person,
orig_comment.community.id,

View file

@ -5,12 +5,7 @@ use lemmy_api_common::{
context::LemmyContext,
post::{CreatePostLike, PostResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_bot_account,
check_community_user_action,
check_downvotes_enabled,
mark_post_as_read,
},
utils::{check_community_user_action, check_vote_permission, mark_post_as_read},
};
use lemmy_db_schema::{
source::{
@ -32,14 +27,21 @@ pub async fn like_post(
) -> Result<Json<PostResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?;
// Don't do a downvote if site has downvotes disabled
check_downvotes_enabled(data.score, &local_site)?;
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?;
// TODO: need to read community both here are for check_community_user_action()
let community = Community::read(&mut context.pool(), post.community_id).await?;
check_vote_permission(
data.score,
&local_site,
&local_user_view.person,
&community,
&context,
)
.await?;
check_community_user_action(
&local_user_view.person,
post.community_id,

View file

@ -9,7 +9,7 @@ use lemmy_db_schema::{
newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId},
source::{
comment::{Comment, CommentUpdateForm},
community::{Community, CommunityModerator, CommunityUpdateForm},
community::{Community, CommunityFollower, CommunityModerator, CommunityUpdateForm},
community_block::CommunityBlock,
email_verification::{EmailVerification, EmailVerificationForm},
instance::Instance,
@ -286,25 +286,6 @@ pub async fn check_person_instance_community_block(
Ok(())
}
#[tracing::instrument(skip_all)]
pub fn check_downvotes_enabled(score: i16, local_site: &LocalSite) -> Result<(), LemmyError> {
if score == -1 && !local_site.enable_downvotes {
Err(LemmyErrorType::DownvotesAreDisabled)?
} else {
Ok(())
}
}
/// Dont allow bots to do certain actions, like voting
#[tracing::instrument(skip_all)]
pub fn check_bot_account(person: &Person) -> Result<(), LemmyError> {
if person.bot_account {
Err(LemmyErrorType::InvalidBotAction)?
} else {
Ok(())
}
}
#[tracing::instrument(skip_all)]
pub fn check_private_instance(
local_user_view: &Option<LocalUserView>,
@ -835,6 +816,36 @@ fn limit_expire_time(expires: DateTime<Utc>) -> LemmyResult<Option<DateTime<Utc>
}
}
/// Enforce various voting restrictions:
/// - Bots are not allowed to vote
/// - Instances can disable downvotes
/// - Communities can prevent users who are not followers from voting
pub async fn check_vote_permission(
score: i16,
local_site: &LocalSite,
person: &Person,
community: &Community,
context: &LemmyContext,
) -> LemmyResult<()> {
// check downvotes enabled
if score == -1 && !local_site.enable_downvotes {
return Err(LemmyErrorType::DownvotesAreDisabled)?;
}
// prevent bots from voting
if person.bot_account {
return Err(LemmyErrorType::InvalidBotAction)?;
}
if community.only_followers_can_vote
&& !CommunityFollower::is_follower(&mut context.pool(), person.id, community.id).await?
{
// TODO: lemmynsfw code checks that follow was at least 10 minutes ago
Err(LemmyErrorType::DownvotesAreDisabled)?
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]

View file

@ -11,17 +11,18 @@ use crate::{
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
newtypes::DbUrl,
newtypes::{CommunityId, DbUrl},
source::{
activity::ActivitySendTargets,
comment::{CommentLike, CommentLikeForm},
community::Community,
local_site::LocalSite,
person::Person,
post::{PostLike, PostLikeForm},
post::{Post, PostLike, PostLikeForm},
},
traits::Likeable,
traits::{Crud, Likeable},
};
use lemmy_utils::error::LemmyError;
use lemmy_utils::error::{LemmyError, LemmyResult};
pub mod undo_vote;
pub mod vote;
@ -67,6 +68,9 @@ async fn vote_comment(
score: vote_type.into(),
};
let person_id = actor.id;
// TODO: inefficient
let post = Post::read(&mut context.pool(), comment.post_id).await?;
check_vote_permission(Some(vote_type), &actor, post.community_id, context).await?;
CommentLike::remove(&mut context.pool(), person_id, comment_id).await?;
CommentLike::like(&mut context.pool(), &like_form).await?;
Ok(())
@ -85,8 +89,9 @@ async fn vote_post(
person_id: actor.id,
score: vote_type.into(),
};
let person_id = actor.id;
PostLike::remove(&mut context.pool(), person_id, post_id).await?;
check_vote_permission(Some(vote_type), &actor, post.community_id, context).await?;
PostLike::remove(&mut context.pool(), actor.id, post_id).await?;
PostLike::like(&mut context.pool(), &like_form).await?;
Ok(())
}
@ -99,6 +104,9 @@ async fn undo_vote_comment(
) -> Result<(), LemmyError> {
let comment_id = comment.id;
let person_id = actor.id;
// TODO: inefficient
let post = Post::read(&mut context.pool(), comment.post_id).await?;
check_vote_permission(None, &actor, post.community_id, context).await?;
CommentLike::remove(&mut context.pool(), person_id, comment_id).await?;
Ok(())
}
@ -111,6 +119,21 @@ async fn undo_vote_post(
) -> Result<(), LemmyError> {
let post_id = post.id;
let person_id = actor.id;
check_vote_permission(None, &actor, post.community_id, context).await?;
PostLike::remove(&mut context.pool(), person_id, post_id).await?;
Ok(())
}
pub async fn check_vote_permission(
vote_type: Option<&VoteType>,
person: &Person,
community_id: CommunityId,
context: &LemmyContext,
) -> LemmyResult<()> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let community = Community::read(&mut context.pool(), community_id).await?;
let score = vote_type.map(|v| v.into()).unwrap_or(0);
lemmy_api_common::utils::check_vote_permission(score, &local_site, &person, &community, &context)
.await?;
Ok(())
}

View file

@ -18,7 +18,7 @@ use activitypub_federation::{
traits::{ActivityHandler, Actor},
};
use anyhow::anyhow;
use lemmy_api_common::{context::LemmyContext, utils::check_bot_account};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_utils::error::LemmyError;
use url::Url;
@ -75,8 +75,6 @@ impl ActivityHandler for Vote {
let actor = self.actor.dereference(context).await?;
let object = self.object.dereference(context).await?;
check_bot_account(&actor.0)?;
match object {
PostOrComment::Post(p) => vote_post(&self.kind, actor, &p, context).await,
PostOrComment::Comment(c) => vote_comment(&self.kind, actor, &c, context).await,

View file

@ -109,6 +109,7 @@ impl Object for ApubCommunity {
updated: self.updated,
posting_restricted_to_mods: Some(self.posting_restricted_to_mods),
attributed_to: Some(generate_moderators_url(&self.actor_id)?.into()),
only_followers_can_vote: Some(self.only_followers_can_vote),
};
Ok(group)
}

View file

@ -74,6 +74,7 @@ pub struct Group {
pub(crate) language: Vec<LanguageTag>,
pub(crate) published: Option<DateTime<Utc>>,
pub(crate) updated: Option<DateTime<Utc>>,
pub(crate) only_followers_can_vote: Option<bool>,
}
impl Group {
@ -122,6 +123,7 @@ impl Group {
posting_restricted_to_mods: self.posting_restricted_to_mods,
instance_id,
featured_url: self.featured.map(Into::into),
only_followers_can_vote: self.only_followers_can_vote,
}
}
@ -152,6 +154,7 @@ impl Group {
moderators_url: self.attributed_to.map(Into::into),
posting_restricted_to_mods: self.posting_restricted_to_mods,
featured_url: self.featured.map(Into::into),
only_followers_can_vote: self.only_followers_can_vote,
}
}
}

View file

@ -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<bool, Error> {
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)),
@ -243,6 +243,19 @@ impl CommunityFollower {
.get_result(conn)
.await
}
pub async fn is_follower(
pool: &mut DbPool<'_>,
person_id: PersonId,
community_id: CommunityId,
) -> Result<bool, Error> {
use crate::schema::community_follower::dsl::community_follower;
let conn = &mut get_conn(pool).await?;
select(exists(community_follower.find((person_id, community_id))))
.get_result(conn)
.await
}
}
impl Queryable<sql_types::Nullable<sql_types::Bool>, Pg> for SubscribedType {

View file

@ -183,6 +183,7 @@ diesel::table! {
moderators_url -> Nullable<Varchar>,
#[max_length = 255]
featured_url -> Nullable<Varchar>,
only_followers_can_vote -> Bool,
}
}

View file

@ -66,6 +66,7 @@ pub struct Community {
/// Url where featured posts collection is served over Activitypub
#[serde(skip)]
pub featured_url: Option<DbUrl>,
pub only_followers_can_vote: bool,
}
#[derive(Debug, Clone, TypedBuilder)]
@ -99,6 +100,7 @@ pub struct CommunityInsertForm {
pub posting_restricted_to_mods: Option<bool>,
#[builder(!default)]
pub instance_id: InstanceId,
pub only_followers_can_vote: Option<bool>,
}
#[derive(Debug, Clone, Default)]
@ -126,6 +128,7 @@ pub struct CommunityUpdateForm {
pub featured_url: Option<DbUrl>,
pub hidden: Option<bool>,
pub posting_restricted_to_mods: Option<bool>,
pub only_followers_can_vote: Option<bool>,
}
#[derive(PartialEq, Eq, Debug)]

@ -1 +1 @@
Subproject commit 2139975ef383077e4709a4f2cae42922fd63b860
Subproject commit 3c217c609aa8826fc725f708221c8b3eb825f41a

View file

@ -1 +0,0 @@
alter table local_site drop column content_warning;

View file

@ -1 +0,0 @@
alter table local_site add column content_warning text;

View file

@ -0,0 +1,2 @@
alter table local_site drop column content_warning;
alter table community drop column only_followers_can_vote;

View file

@ -0,0 +1,2 @@
alter table local_site add column content_warning text;
alter table community add column only_followers_can_vote boolean not null default false;