0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-09 21:23:14 +08:00
discourse/plugins/discourse-reactions/app/services/discourse_reactions/reaction_manager.rb
Régis Hanol 20e4134a09
FEATURE: Prevent silenced users from liking and using reactions (#37040)
Silenced users can now no longer like posts or use reactions, which
closes a potential griefing vector that was difficult for moderators
to monitor.

The implementation adds a silenced check to the guardian's post_can_act?
method for likes, and introduces a new can_use_reactions? guardian method
in the discourse-reactions plugin that delegates to the same logic. This
ensures both features share the same authorization path.

Additionally, silenced users' custom status is now shadow-banned: visible
to themselves and staff, but hidden from other users.

A new `can_see_user_status?` guardian method centralizes the visibility
logic, used by serializers and MessageBus publishing. Status updates
from silenced users are now only broadcast to themselves and staff.

Also includes minor CSS fixes for user status spacing and alignment.

Chat reactions already had proper silenced user checks in place via the
can_react? guardian method, so no changes were needed there.

Ref - t/140084
2026-01-13 13:59:57 +01:00

147 lines
4.1 KiB
Ruby
Vendored

# frozen_string_literal: true
module DiscourseReactions
class ReactionManager
attr_reader :reaction_value, :previous_reaction_value
def initialize(reaction_value:, user:, post:)
@reaction_value = reaction_value
@user = user
@post = post
@like =
@post.post_actions.find_by(
user: @user,
post_action_type_id: PostActionType::LIKE_POST_ACTION_ID,
)
@previous_reaction_value =
if @like && !reaction_user
DiscourseReactions::Reaction.main_reaction_id
elsif reaction_user
old_reaction_value(reaction_user)
end
end
def toggle!
raise Discourse::InvalidAccess unless @user.guardian.can_use_reactions?(@post)
if (@like && !@user.guardian.can_delete_post_action?(@like)) ||
(reaction_user && !@user.guardian.can_delete_reaction_user?(reaction_user))
raise Discourse::InvalidAccess
end
ActiveRecord::Base.transaction do
@reaction = reaction_scope&.first_or_create
@reaction_user = reaction_user_scope
if @reaction_value == DiscourseReactions::Reaction.main_reaction_id
toggle_like
else
toggle_reaction
end
end
end
private
def toggle_like
if reaction_user.present?
remove_reaction
@reaction = reaction_scope&.first_or_create
add_reaction
else
@like ? remove_shadow_like : add_shadow_like
end
end
def toggle_reaction
if reaction_user.present?
remove_reaction
return if previous_reaction_value && previous_reaction_value == @reaction_value
end
remove_shadow_like if @like
add_reaction if reaction_user.blank?
end
def add_reaction_notification
DiscourseReactions::ReactionNotification.new(@reaction, @user).create
end
def remove_reaction_notification
DiscourseReactions::ReactionNotification.new(@reaction, @user).delete
end
def reaction_scope
DiscourseReactions::Reaction.where(
post_id: @post.id,
reaction_value: @reaction_value,
reaction_type: DiscourseReactions::Reaction.reaction_types["emoji"],
)
end
def reaction_user_scope
return nil unless @reaction
search_reaction_user =
DiscourseReactions::ReactionUser.where(user_id: @user.id, post_id: @post.id)
create_reaction_user =
DiscourseReactions::ReactionUser.new(
reaction_id: @reaction.id,
user_id: @user.id,
post_id: @post.id,
)
if search_reaction_user.length > 0
search_reaction_user.first
else
create_reaction_user
end
end
def reaction_user
DiscourseReactions::ReactionUser.find_by(user_id: @user.id, post_id: @post.id)
end
def old_reaction_value(reaction_user)
return unless reaction_user
DiscourseReactions::Reaction.where(id: reaction_user.reaction_id).first&.reaction_value
end
def add_shadow_like(notify: true)
silent = true
PostActionCreator.like(@user, @post, silent)
add_reaction_notification if notify
end
def remove_shadow_like
PostActionDestroyer.new(@user, @post, PostActionType::LIKE_POST_ACTION_ID).perform
delete_like_reaction
remove_reaction_notification
end
def delete_like_reaction
DiscourseReactions::Reaction.where(
reaction_value: DiscourseReactions::Reaction.main_reaction_id,
post_id: @post.id,
).destroy_all
end
def add_reaction
@reaction_user = reaction_user_scope if reaction_user.blank?
@reaction_user.save!
add_shadow_like(notify: false) if !reaction_excluded_from_like?
add_reaction_notification
end
def remove_reaction
@reaction_user.destroy
remove_shadow_like
delete_reaction_with_no_users
end
def delete_reaction_with_no_users
DiscourseReactions::Reaction.where(reaction_users_count: 0, post_id: @post.id).destroy_all
end
def reaction_excluded_from_like?
DiscourseReactions::Reaction.reactions_excluded_from_like.include?(@reaction_value)
end
end
end