FEATURE: Implement SiteSetting to Allow Anonymous Likes (#22131)

Allow anonymous users (logged-in, but set to anonymous posting) to like posts

---------

Co-authored-by: Emmett Ling <eling@zendesk.com>
Co-authored-by: Nat <natalie.tay@discourse.org>
This commit is contained in:
Emmett Ling 2023-07-21 06:21:07 -07:00 committed by GitHub
parent 8ffc274438
commit 978d52841a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 221 additions and 4 deletions

View File

@ -315,8 +315,14 @@ class PostSerializer < BasicPostSerializer
summary.delete(:can_act) summary.delete(:can_act)
end end
if actions.present? && SiteSetting.allow_anonymous_likes && sym == :like &&
!scope.can_delete_post_action?(actions[id])
summary.delete(:can_act)
end
if actions.present? && actions.has_key?(id) if actions.present? && actions.has_key?(id)
summary[:acted] = true summary[:acted] = true
summary[:can_undo] = true if scope.can_delete?(actions[id]) summary[:can_undo] = true if scope.can_delete?(actions[id])
end end

View File

@ -2164,6 +2164,7 @@ en:
enable_category_group_moderation: "Allow groups to moderate content in specific categories" enable_category_group_moderation: "Allow groups to moderate content in specific categories"
group_in_subject: "Set %%{optional_pm} in email subject to name of first group in PM, see: <a href='https://meta.discourse.org/t/customize-specific-email-templates/88323' target='_blank'>Customize subject format for standard emails</a>" group_in_subject: "Set %%{optional_pm} in email subject to name of first group in PM, see: <a href='https://meta.discourse.org/t/customize-specific-email-templates/88323' target='_blank'>Customize subject format for standard emails</a>"
allow_anonymous_posting: "Allow users to switch to anonymous mode" allow_anonymous_posting: "Allow users to switch to anonymous mode"
allow_anonymous_likes: "Allow anonymous users to like posts"
anonymous_posting_min_trust_level: "Minimum trust level required to enable anonymous posting" anonymous_posting_min_trust_level: "Minimum trust level required to enable anonymous posting"
anonymous_account_duration_minutes: "To protect anonymity create a new anonymous account every N minutes for each user. Example: if set to 600, as soon as 600 minutes elapse from last post AND user switches to anon, a new anonymous account is created." anonymous_account_duration_minutes: "To protect anonymity create a new anonymous account every N minutes for each user. Example: if set to 600, as soon as 600 minutes elapse from last post AND user switches to anon, a new anonymous account is created."

View File

@ -671,6 +671,9 @@ users:
allow_anonymous_posting: allow_anonymous_posting:
default: false default: false
client: true client: true
allow_anonymous_likes:
default: false
client: true
anonymous_posting_min_trust_level: anonymous_posting_min_trust_level:
default: 1 default: 1
enum: "TrustLevelSetting" enum: "TrustLevelSetting"

View File

@ -620,10 +620,14 @@ class Guardian
private private
def is_my_own?(obj) def is_my_own?(obj)
unless anonymous? if anonymous?
return obj.user_id == @user.id if obj.respond_to?(:user_id) && obj.user_id && @user.id return(
return obj.user == @user if obj.respond_to?(:user) SiteSetting.allow_anonymous_likes? && obj.class == PostAction && obj.is_like? &&
obj.user_id == @user.id
)
end end
return obj.user_id == @user.id if obj.respond_to?(:user_id) && obj.user_id && @user.id
return obj.user == @user if obj.respond_to?(:user)
false false
end end

View File

@ -43,7 +43,10 @@ module PostGuardian
already_did_flagging = taken.any? && (taken & PostActionType.notify_flag_types.values).any? already_did_flagging = taken.any? && (taken & PostActionType.notify_flag_types.values).any?
result = result =
if authenticated? && post && !@user.anonymous? if authenticated? && post
# Allow anonymous users to like if feature is enabled and short-circuit otherwise
return SiteSetting.allow_anonymous_likes? && (action_key == :like) if @user.anonymous?
# Silenced users can't flag # Silenced users can't flag
return false if is_flag && @user.silenced? return false if is_flag && @user.silenced?

View File

@ -98,6 +98,38 @@ RSpec.describe Guardian do
fab!(:user) { Fabricate(:user) } fab!(:user) { Fabricate(:user) }
fab!(:post) { Fabricate(:post) } fab!(:post) { Fabricate(:post) }
describe "an anonymous user" do
before { SiteSetting.allow_anonymous_posting = true }
context "when allow_anonymous_likes is enabled" do
before { SiteSetting.allow_anonymous_likes = true }
it "returns true when liking" do
expect(Guardian.new(anonymous_user).post_can_act?(post, :like)).to be_truthy
end
it "cannot perform any other action" do
expect(Guardian.new(anonymous_user).post_can_act?(post, :flag)).to be_falsey
expect(Guardian.new(anonymous_user).post_can_act?(post, :bookmark)).to be_falsey
expect(Guardian.new(anonymous_user).post_can_act?(post, :notify_user)).to be_falsey
end
end
context "when allow_anonymous_likes is disabled" do
before { SiteSetting.allow_anonymous_likes = false }
it "returns false when liking" do
expect(Guardian.new(anonymous_user).post_can_act?(post, :like)).to be_falsey
end
it "cannot perform any other action" do
expect(Guardian.new(anonymous_user).post_can_act?(post, :flag)).to be_falsey
expect(Guardian.new(anonymous_user).post_can_act?(post, :bookmark)).to be_falsey
expect(Guardian.new(anonymous_user).post_can_act?(post, :notify_user)).to be_falsey
end
end
end
it "returns false when the user is nil" do it "returns false when the user is nil" do
expect(Guardian.new(nil).post_can_act?(post, :like)).to be_falsey expect(Guardian.new(nil).post_can_act?(post, :like)).to be_falsey
end end
@ -2443,6 +2475,122 @@ RSpec.describe Guardian do
end end
end end
describe "#can_delete_post_action" do
before do
SiteSetting.allow_anonymous_posting = true
Guardian.any_instance.stubs(:anonymous?).returns(true)
end
context "with allow_anonymous_likes enabled" do
before { SiteSetting.allow_anonymous_likes = true }
describe "an anonymous user" do
let(:post_action) do
user.id = anonymous_user.id
post.id = 1
a =
PostAction.new(
user: anonymous_user,
post: post,
post_action_type_id: PostActionType.types[:like],
)
a.created_at = 1.minute.ago
a
end
let(:non_like_post_action) do
user.id = anonymous_user.id
post.id = 1
a =
PostAction.new(
user: anonymous_user,
post: post,
post_action_type_id: PostActionType.types[:reply],
)
a.created_at = 1.minute.ago
a
end
let(:other_users_post_action) do
user.id = user.id
post.id = 1
a =
PostAction.new(user: user, post: post, post_action_type_id: PostActionType.types[:like])
a.created_at = 1.minute.ago
a
end
it "returns true if the post belongs to the anonymous user" do
expect(Guardian.new(anonymous_user).can_delete_post_action?(post_action)).to be_truthy
end
it "return false if the post belongs to another user" do
expect(
Guardian.new(anonymous_user).can_delete_post_action?(other_users_post_action),
).to be_falsey
end
it "returns false for any other action" do
expect(
Guardian.new(anonymous_user).can_delete_post_action?(non_like_post_action),
).to be_falsey
end
it "returns false if the window has expired" do
post_action.created_at = 20.minutes.ago
SiteSetting.post_undo_action_window_mins = 10
expect(Guardian.new(anonymous_user).can_delete?(post_action)).to be_falsey
end
end
end
context "with allow_anonymous_likes disabled" do
before do
SiteSetting.allow_anonymous_likes = false
SiteSetting.allow_anonymous_posting = true
end
describe "an anonymous user" do
let(:post_action) do
user.id = anonymous_user.id
post.id = 1
a =
PostAction.new(
user: anonymous_user,
post: post,
post_action_type_id: PostActionType.types[:like],
)
a.created_at = 1.minute.ago
a
end
let(:non_like_post_action) do
user.id = anonymous_user.id
post.id = 1
a =
PostAction.new(
user: anonymous_user,
post: post,
post_action_type_id: PostActionType.types[:reply],
)
a.created_at = 1.minute.ago
a
end
it "any action returns false" do
expect(Guardian.new(anonymous_user).can_delete_post_action?(post_action)).to be_falsey
expect(
Guardian.new(anonymous_user).can_delete_post_action?(non_like_post_action),
).to be_falsey
end
end
end
end
describe "#can_see_deleted_posts?" do describe "#can_see_deleted_posts?" do
it "returns true if the user is an admin" do it "returns true if the user is an admin" do
expect(Guardian.new(admin).can_see_deleted_posts?(post.topic.category)).to be_truthy expect(Guardian.new(admin).can_see_deleted_posts?(post.topic.category)).to be_truthy

View File

@ -310,6 +310,58 @@ RSpec.describe PostSerializer do
end end
end end
context "with allow_anonymous_likes enabled" do
fab!(:user) { Fabricate(:user) }
fab!(:topic) { Fabricate(:topic, user: user) }
fab!(:post) { Fabricate(:post, topic: topic, user: topic.user) }
fab!(:anonymous_user) { Fabricate(:anonymous) }
let(:serializer) { PostSerializer.new(post, scope: Guardian.new(anonymous_user), root: false) }
let(:post_action) do
user.id = anonymous_user.id
post.id = 1
a =
PostAction.new(
user: anonymous_user,
post: post,
post_action_type_id: PostActionType.types[:like],
)
a.created_at = 1.minute.ago
a
end
before do
SiteSetting.allow_anonymous_posting = true
SiteSetting.allow_anonymous_likes = true
SiteSetting.post_undo_action_window_mins = 10
PostSerializer.any_instance.stubs(:post_actions).returns({ 2 => post_action })
end
context "when post_undo_action_window_mins has not passed" do
before { post_action.created_at = 5.minutes.ago }
it "allows anonymous users to unlike posts" do
like_actions_summary =
serializer.actions_summary.find { |a| a[:id] == PostActionType.types[:like] }
#When :can_act is present, the JavaScript allows the user to click the unlike button
expect(like_actions_summary[:can_act]).to eq(true)
end
end
context "when post_undo_action_window_mins has passed" do
before { post_action.created_at = 20.minutes.ago }
it "disallows anonymous users from unliking posts" do
# There are no other post actions available to anonymous users so the action_summary will be an empty array
expect(serializer.actions_summary.find { |a| a[:id] == PostActionType.types[:like] }).to eq(
nil,
)
end
end
end
describe "#user_status" do describe "#user_status" do
fab!(:user_status) { Fabricate(:user_status) } fab!(:user_status) { Fabricate(:user_status) }
fab!(:user) { Fabricate(:user, user_status: user_status) } fab!(:user) { Fabricate(:user, user_status: user_status) }