Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions crates/api/api/src/comment/like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ use lemmy_api_utils::{
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_bot_account, check_community_user_action, check_local_vote_mode},
utils::{
check_bot_account,
check_community_user_action,
check_local_user_banned_or_deleted,
check_vote_settings,
},
};
use lemmy_db_schema::{
source::{
Expand All @@ -21,7 +26,6 @@ use lemmy_db_views_comment::{
api::{CommentResponse, CreateCommentLike},
};
use lemmy_db_views_local_user::LocalUserView;
use lemmy_db_views_site::SiteView;
use lemmy_utils::error::LemmyResult;
use std::ops::Deref;

Expand All @@ -30,28 +34,29 @@ pub async fn like_comment(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> {
let local_site = SiteView::read_local(&mut context.pool()).await?.local_site;
check_local_user_banned_or_deleted(&local_user_view)?;
let local_instance_id = local_user_view.person.instance_id;
let comment_id = data.comment_id;
let my_person_id = local_user_view.person.id;

check_local_vote_mode(
data.is_upvote,
PostOrCommentId::Comment(comment_id),
&local_site,
my_person_id,
&mut context.pool(),
)
.await?;
check_bot_account(&local_user_view.person)?;

let orig_comment = CommentView::read(
&mut context.pool(),
comment_id,
Some(&local_user_view.local_user),
local_instance_id,
)
.await?;

check_vote_settings(
data.is_upvote,
PostOrCommentId::Comment(comment_id),
&orig_comment.community,
&local_user_view.person,
&context,
)
.await?;
check_bot_account(&local_user_view.person)?;

let previous_is_upvote = orig_comment.comment_actions.and_then(|p| p.vote_is_upvote);

check_community_user_action(
Expand Down
32 changes: 18 additions & 14 deletions crates/api/api/src/post/like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ use lemmy_api_utils::{
context::LemmyContext,
plugins::{plugin_hook_after, plugin_hook_before},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_bot_account, check_community_user_action, check_local_vote_mode},
utils::{
check_bot_account,
check_community_user_action,
check_local_user_banned_or_deleted,
check_vote_settings,
},
};
use lemmy_db_schema::{
source::{
Expand All @@ -21,7 +26,6 @@ use lemmy_db_views_post::{
PostView,
api::{CreatePostLike, PostResponse},
};
use lemmy_db_views_site::SiteView;
use lemmy_utils::error::LemmyResult;
use std::ops::Deref;

Expand All @@ -30,22 +34,11 @@ pub async fn like_post(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> {
let local_site = SiteView::read_local(&mut context.pool()).await?.local_site;
check_local_user_banned_or_deleted(&local_user_view)?;
let local_instance_id = local_user_view.person.instance_id;
let post_id = data.post_id;
let my_person_id = local_user_view.person.id;

check_local_vote_mode(
data.is_upvote,
PostOrCommentId::Post(post_id),
&local_site,
my_person_id,
&mut context.pool(),
)
.await?;
check_bot_account(&local_user_view.person)?;

// Check for a community ban
let orig_post = PostView::read(
&mut context.pool(),
post_id,
Expand All @@ -54,6 +47,17 @@ pub async fn like_post(
false,
)
.await?;
check_bot_account(&local_user_view.person)?;

check_vote_settings(
data.is_upvote,
PostOrCommentId::Post(post_id),
&orig_post.community,
&local_user_view.person,
&context,
)
.await?;

let previous_is_upvote = orig_post.post_actions.and_then(|p| p.vote_is_upvote);

check_community_user_action(&local_user_view, &orig_post.community, &mut context.pool()).await?;
Expand Down
2 changes: 1 addition & 1 deletion crates/api/api_common/src/community.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub use lemmy_db_schema::source::{
multi_community::{MultiCommunity, MultiCommunityFollow},
};
pub use lemmy_db_schema_file::{
enums::CommunityVisibility,
enums::{CommunityVisibility, VoteSettings},
newtypes::{CommunityId, CommunityTagId, MultiCommunityId},
};
pub use lemmy_db_views_community::{
Expand Down
2 changes: 1 addition & 1 deletion crates/api/api_common/src/federation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub use lemmy_db_schema::source::{
federation_queue_state::FederationQueueState,
instance::{Instance, InstanceActions},
};
pub use lemmy_db_schema_file::{InstanceId, enums::FederationMode, newtypes::ActivityId};
pub use lemmy_db_schema_file::{InstanceId, enums::VoteSettings, newtypes::ActivityId};
pub use lemmy_db_views_site::{
FederatedInstanceView,
api::{
Expand Down
1 change: 1 addition & 0 deletions crates/api/api_crud/src/community/create.rs

@dessalines dessalines Jul 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where these are being copied over from the local site vote settings, which is critical. See here

@Nutomic Nutomic Sep 22, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Also renamed to community.post_downvote_mode to clarify it, and potentially add other settings later.

Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ pub async fn create_community(
featured_url: Some(generate_featured_url(&ap_id)?),
posting_restricted_to_mods: data.posting_restricted_to_mods,
visibility: data.visibility,
post_downvote_mode: Some(data.post_downvote_mode.unwrap_or(local_site.post_downvotes)),
..CommunityInsertForm::new(site.instance_id, data.name.clone(), keypair.public_key)
};

Expand Down
1 change: 1 addition & 0 deletions crates/api/api_crud/src/community/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ pub async fn edit_community(
nsfw: data.nsfw,
posting_restricted_to_mods: data.posting_restricted_to_mods,
visibility: data.visibility,
post_downvote_mode: data.post_downvote_mode,
updated_at: Some(Some(Utc::now())),
..Default::default()
};
Expand Down
67 changes: 44 additions & 23 deletions crates/api/api_utils/src/utils.rs

@dessalines dessalines Jul 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the logical changes in here, especially removing the item type, and vote type, need to be reverted.

The original reason for this functionality, was that some sites might want to disable all voting entirely, or only disable votes for posts (or comments), or only allow them under certain federation modes. This removes the ability for instances to do that.

There's also good reasons why a site (or community) might only want to allow upvotes to a certain mode (IE local).

I think the best way forward might just be to dupe the existing site vote settings over to community, and on making new communities, copy the existing site vote settings over initially, but let communities tweak them. And maybe rename the site settings default_post_... . This would let communities override the site settings, but still use them as a default / suggestion.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As these settings dont need to be federated, they can actually be implemented as a plugin which is very much preferable. A plugin is much more flexible, for example it can be configured to allow downvotes only from a specific set of instances, or reject votes from users that havent posted before. Additionally plugins are much easier to change and its not necessary to make a new Lemmy release each time.

Have a look at the example plugin here: https://github.com/LemmyNet/lemmy-plugins/blob/master/plugins/rust_allowed_voters/src/lib.rs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the idea of moving what should be core functionality (whether to allow upvotes and downvotes on certain items), to plugins. There are some pretty large existing lemmy sites who like the ability to disable downvotes, and I'd rather not try to move that to a plugin people have to add and configure.

We should of course keep those plugin abilities, but it shouldn't replace these settings IMO.

If we did my suggestion above : copy the local site vote settings for any new communities created, it would solve the federation issue.

@Nutomic Nutomic Jul 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is by no means a core feature because it can be implemented very easily in a plugin. Core features are things that cant be implemented as plugins, for example things that federate or new API endpoints. I have updated the existing allowed_votes plugin to add these same voting modes (LemmyNet/lemmy-plugins#11). As you can see it can even do much more, like rejecting votes from users who made less than x posts in total.

Whether the functionality is implemented as a plugin or directly in Lemmy, it always needs to be configured. For now the plugin config may be a bit difficult because its done in the config file, but it will be much easier when #6487 is implemented. And then we can easily have a page in lemmy-ui to add plugins and set parameters. That way not every little thing has to become a core feature, plus it will be much more flexible. For example you could easily implement a downvote allowlist, so that only users from specific instances can downvote posts. And then deploy it immediately, without waiting for a new Lemmy release.

@dessalines dessalines Jul 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should always prefer integrating things into lemmy's core over plugins. Plugins might be more configurable, but they're a power user feature.

Admins are just going to want to click a button to set their voting modes, whether ppl can upvote or downvote, and on what items. This should be a core part of lemmy, as I already did the work for this already.

Your plugin should account for the 4 settings: post upvote, post downvote, comment upvote, comment downvote.

@dessalines dessalines Jul 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing I realized: these vote settings must come back with GetSite anyway, because UI elements have to be hidden based on them. I already did all the work for this in the front end.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I restored the LocalSite settings in order to avoid breaking changes. Still, if people ask for more vote settings in the future (or settings for post/comment permissions), these should be implemented in plugins. Not everything related to votes or posts can be a core feature, and we cant support hundreds of LocalSite settings.

It is absolutely possible to return plugin settings from LocalSite by adding a plugin hook there, and changing the frontend behaviour based on that. Doing that will require some extra work, but also result in more flexibility.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use chrono::{DateTime, Days, Local, TimeZone, Utc};
use enum_map::{EnumMap, enum_map};
use lemmy_db_schema::{
source::{
comment::{Comment, CommentActions, CommentLikeForm},
comment::{Comment, CommentActions},
community::{Community, CommunityActions, CommunityUpdateForm},
community_tag::{CommunityTag, PostCommunityTag},
images::{ImageDetails, RemoteImage},
Expand All @@ -22,7 +22,7 @@ use lemmy_db_schema::{
modlog::{Modlog, ModlogInsertForm},
oauth_account::OAuthAccount,
person::{Person, PersonUpdateForm},
post::{Post, PostActions, PostLikeForm, PostReadCommentsForm},
post::{Post, PostActions, PostReadCommentsForm},
private_message::PrivateMessage,
registration_application::RegistrationApplication,
site::Site,
Expand All @@ -32,7 +32,7 @@ use lemmy_db_schema::{
use lemmy_db_schema_file::{
InstanceId,
PersonId,
enums::{FederationMode, ImageMode, RegistrationMode},
enums::{CommunityFollowerState, ImageMode, RegistrationMode, VoteSettings},
newtypes::{CommunityId, CommunityTagId, ModlogId, PostId, PostOrCommentId},
};
use lemmy_db_views_community_follower_approval::PendingFollowerView;
Expand Down Expand Up @@ -296,35 +296,56 @@ pub fn check_comment_deleted_or_removed(comment: &Comment) -> LemmyResult<()> {
}
}

pub async fn check_local_vote_mode(
is_upvote: Option<bool>,
/// Checks whether a vote is allowed, considering both the local site's federation vote settings
/// (per post/comment) and, for post downvotes, the community's post downvote setting.
pub async fn check_vote_settings(
vote: Option<bool>,
post_or_comment_id: PostOrCommentId,
local_site: &LocalSite,
person_id: PersonId,
pool: &mut DbPool<'_>,
community: &Community,
person: &Person,
context: &LemmyContext,
) -> LemmyResult<()> {
let is_upvote = vote.unwrap_or_default();
let local_site = SiteView::read_local(&mut context.pool())
.await
.map(|s| s.local_site)
.unwrap_or_default();

let (downvote_setting, upvote_setting) = match post_or_comment_id {
PostOrCommentId::Post(_) => (local_site.post_downvotes, local_site.post_upvotes),
PostOrCommentId::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes),
};

let downvote_fail = is_upvote == Some(false) && downvote_setting == FederationMode::Disable;
let upvote_fail = is_upvote == Some(true) && upvote_setting == FederationMode::Disable;

// Undo previous vote for item if new vote fails
if downvote_fail || upvote_fail {
match post_or_comment_id {
PostOrCommentId::Post(post_id) => {
let form = PostLikeForm::new(post_id, person_id, None);
PostActions::like(pool, &form).await?;
}
PostOrCommentId::Comment(comment_id) => {
let form = CommentLikeForm::new(comment_id, person_id, None);
CommentActions::like(pool, &form).await?;
let check_vote_fn = async |vote_setting, context: &LemmyContext| {
Ok::<bool, LemmyError>(match vote_setting {
VoteSettings::All => true,
VoteSettings::Local => person.local,
VoteSettings::Subscribed => {
let actions = CommunityActions::read(&mut context.pool(), community.id, person.id).await?;
actions.followed_at.is_some()
&& actions.follow_state == Some(CommunityFollowerState::Accepted)
}
};
VoteSettings::Disable => false,
})
};
let site_allowed = if is_upvote {
check_vote_fn(upvote_setting, context).await?
} else {
check_vote_fn(downvote_setting, context).await?
};

// The community vote setting only applies to post downvotes.
let community_allowed = if !is_upvote && let PostOrCommentId::Post(_) = post_or_comment_id {
check_vote_fn(community.post_downvote_mode, context).await?
} else {
true
};

if site_allowed && community_allowed {
Ok(())
} else {
Err(UntranslatedError::VoteNotAllowed.into())
}
Ok(())
}

/// Dont allow bots to do certain actions, like voting
Expand Down
46 changes: 18 additions & 28 deletions crates/apub/activities/src/voting/vote.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
generate_activity_id,
protocol::voting::vote::{Vote, VoteType},
voting::{undo_vote_comment, undo_vote_post, vote_comment, vote_post},
voting::{vote_comment, vote_post},
};
use activitypub_federation::{
config::Data,
Expand All @@ -10,14 +10,13 @@ use activitypub_federation::{
};
use lemmy_api_utils::{
context::LemmyContext,
utils::{check_bot_account, check_community_deleted_removed},
utils::{check_bot_account, check_community_deleted_removed, check_vote_settings},
};
use lemmy_apub_objects::{
objects::{PostOrComment, community::ApubCommunity, person::ApubPerson},
utils::{functions::verify_person_in_community, protocol::InCommunity},
};
use lemmy_db_schema_file::enums::FederationMode;
use lemmy_db_views_site::SiteView;
use lemmy_db_schema_file::newtypes::PostOrCommentId;
use lemmy_utils::error::{LemmyError, LemmyResult};
use url::Url;

Expand Down Expand Up @@ -62,36 +61,27 @@ impl Activity for Vote {
async fn receive(self, context: &Data<LemmyContext>) -> LemmyResult<()> {
let actor = self.actor.dereference(context).await?;
let object = self.object.dereference(context).await?;
let community = self.community(context).await?;

check_bot_account(&actor.0)?;

// Check for enabled federation votes
let local_site = SiteView::read_local(&mut context.pool())
.await
.map(|s| s.local_site)
.unwrap_or_default();

let (downvote_setting, upvote_setting) = match object {
PostOrComment::Left(_) => (local_site.post_downvotes, local_site.post_upvotes),
PostOrComment::Right(_) => (local_site.comment_downvotes, local_site.comment_upvotes),
let post_or_comment_id = match &object {
PostOrComment::Left(p) => PostOrCommentId::Post(p.id),
PostOrComment::Right(c) => PostOrCommentId::Comment(c.id),
};

// 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;
check_vote_settings(
Some(self.kind == VoteType::Like),
post_or_comment_id,
&community,
&actor,
context,
)
.await?;

if downvote_fail || upvote_fail {
// If this is a rejection, undo the vote
match object {
PostOrComment::Left(p) => undo_vote_post(actor, &p, context).await,
PostOrComment::Right(c) => undo_vote_comment(actor, &c, context).await,
}
} else {
// Otherwise apply the vote normally
match object {
PostOrComment::Left(p) => vote_post(&self.kind, actor, &p, context).await,
PostOrComment::Right(c) => vote_comment(&self.kind, actor, &c, context).await,
}
match object {
PostOrComment::Left(p) => vote_post(&self.kind, actor, &p, context).await,
PostOrComment::Right(c) => vote_comment(&self.kind, actor, &c, context).await,
}
}
}
2 changes: 2 additions & 0 deletions crates/apub/objects/src/objects/community.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ impl Object for ApubCommunity {
.into_iter()
.map(ApubCommunityTag::to_json)
.collect(),
post_downvote_mode: Some(self.post_downvote_mode),
};
Ok(group)
}
Expand Down Expand Up @@ -242,6 +243,7 @@ impl Object for ApubCommunity {
featured_url: group.featured.clone().clone().map(Into::into),
title,
visibility,
post_downvote_mode: Some(group.post_downvote_mode.unwrap_or_default()),
..CommunityInsertForm::new(instance_id, name, group.public_key.public_key_pem.clone())
};
let languages =
Expand Down
3 changes: 3 additions & 0 deletions crates/apub/objects/src/protocol/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use activitypub_federation::{
},
};
use chrono::{DateTime, Utc};
use lemmy_db_schema_file::enums::VoteSettings;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::fmt::Debug;
Expand Down Expand Up @@ -64,4 +65,6 @@ pub struct Group {
pub(crate) discoverable: Option<bool>,
#[serde(deserialize_with = "deserialize_skip_error", default)]
pub(crate) tag: Vec<ApubCommunityTag>,
// lemmy extension
pub(crate) post_downvote_mode: Option<VoteSettings>,
}
1 change: 1 addition & 0 deletions crates/db_schema/src/impls/community.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@ mod tests {
unresolved_report_count: 0,
interactions_month: 0,
local_removed: false,
post_downvote_mode: Default::default(),
};

let community_follower_form = CommunityFollowerForm::new(
Expand Down
Loading