Add sink user service and specs for mass vomits (#6006)

* Add sink user service and specs for mass vomits

* Use new score calculation

* Lower articles' scores instead and make async

* Add lower score functionality to controllers

* Remove unused variable

* Use correct var and add missing arg

* Remove unused status

* Add composite index for reactable_type and ID

* Use alias instead of additional boolean

* Use medium priority for sink worker

* Log to honeybadger and not logs

* Log the error and not a string
This commit is contained in:
Andy Zhao 2020-02-17 10:29:50 -05:00 committed by GitHub
parent 6356089ee0
commit acd3b92b32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 84 additions and 5 deletions

View file

@ -2,6 +2,13 @@ class Internal::ReactionsController < Internal::ApplicationController
def update
@reaction = Reaction.find(params[:id])
@reaction.update(status: params[:reaction][:status])
Moderator::SinkArticles.call(@reaction.user_id) if confirmed_vomit_reaction?
redirect_to "/internal/reports"
end
private
def confirmed_vomit_reaction?
@reaction.reactable_type == "User" && @reaction.status == "confirmed" && @reaction.category == "vomit"
end
end

View file

@ -51,6 +51,7 @@ class ReactionsController < ApplicationController
if reaction
current_user.touch
reaction.destroy
Moderator::SinkArticles.call(reaction.user_id) if vomit_reaction_on_user?(reaction)
Notification.send_reaction_notification_without_delay(reaction, reaction.reactable.user)
Notification.send_reaction_notification_without_delay(reaction, reaction.reactable.organization) if organization_article?(reaction)
@result = "destroy"
@ -62,6 +63,7 @@ class ReactionsController < ApplicationController
category: category,
)
@result = "create"
Moderator::SinkArticles.call(reaction.user_id) if vomit_reaction_on_user?(reaction)
Notification.send_reaction_notification(reaction, reaction.reactable.user)
Notification.send_reaction_notification(reaction, reaction.reactable.organization) if organization_article?(reaction)
end
@ -80,4 +82,8 @@ class ReactionsController < ApplicationController
def organization_article?(reaction)
reaction.reactable_type == "Article" && reaction.reactable.organization.present?
end
def vomit_reaction_on_user?(reaction)
reaction.reactable_type == "User" && reaction.category == "vomit"
end
end

View file

@ -394,7 +394,8 @@ class Article < ApplicationRecord
end
def update_score
update_columns(score: reactions.sum(:points),
new_score = reactions.sum(:points) + Reaction.where(reactable_id: user_id, reactable_type: "User").sum(:points)
update_columns(score: new_score,
comment_score: comments.sum(:score),
hotness_score: BlackBox.article_hotness_score(self),
spaminess_rating: BlackBox.calculate_spaminess(self))

View file

@ -153,6 +153,7 @@ class Reaction < ApplicationRecord
def assign_points
base_points = BASE_POINTS.fetch(category, 1.0)
base_points = 0 if status == "invalid"
base_points /= 2 if reactable_type == "User"
base_points *= 2 if status == "confirmed"
self.points = user ? (base_points * user.reputation_modifier) : -5
end

View file

@ -154,6 +154,8 @@ class User < ApplicationRecord
validate :non_banished_username, :username_changed?
validate :unique_including_orgs_and_podcasts, if: :username_changed?
alias_attribute :positive_reactions_count, :reactions_count
scope :dev_account, -> { find_by(id: SiteConfig.staff_user_id) }
scope :welcoming_account, -> { find_by(id: ApplicationConfig["WELCOMING_USER_ID"]) }
@ -628,10 +630,6 @@ class User < ApplicationRecord
def featured_number; end
def positive_reactions_count
reactions_count
end
def user
self
end

View file

@ -0,0 +1,7 @@
module Moderator
class SinkArticles
def self.call(user_id)
Moderator::SinkArticlesWorker.perform_async(user_id)
end
end
end

View file

@ -0,0 +1,20 @@
module Moderator
class SinkArticlesWorker
include Sidekiq::Worker
sidekiq_options queue: :medium_priority, retry: 10
def perform(user_id)
user = User.find_by(id: user_id)
return unless user
articles = Article.where(user: user)
reactions = Reaction.where(reactable: articles)
new_score = reactions.sum(:points) + Reaction.where(reactable: user).sum(:points)
articles.update_all(score: new_score)
rescue StandardError => e
DataDogStatsClient.count("moderators.sink", 1, tags: ["action:failed", "user_id:#{user.id}"])
Honeybadger.notify(e)
end
end
end

View file

@ -0,0 +1,7 @@
class AddReactableTypeReactableIdIndexToReactions < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_index :reactions, %i[reactable_id reactable_type], algorithm: :concurrently
end
end

View file

@ -867,6 +867,7 @@ ActiveRecord::Schema.define(version: 2020_02_13_182938) do
t.index ["category"], name: "index_reactions_on_category"
t.index ["created_at"], name: "index_reactions_on_created_at"
t.index ["points"], name: "index_reactions_on_points"
t.index ["reactable_id", "reactable_type"], name: "index_reactions_on_reactable_id_and_reactable_type"
t.index ["reactable_id"], name: "index_reactions_on_reactable_id"
t.index ["reactable_type"], name: "index_reactions_on_reactable_type"
t.index ["user_id"], name: "index_reactions_on_user_id"

View file

@ -0,0 +1,31 @@
require "rails_helper"
RSpec.describe Moderator::SinkArticles, type: :service do
let(:moderator) { create(:user, :trusted) }
let(:spam_user) do
user = create(:user)
create_list(:article, 3, user: user)
user
end
let(:vomit_reaction) { create(:reaction, reactable: spam_user, user: moderator, category: "vomit") }
describe "#call" do
it "lowers all of a user's articles' scores by 25 each if not confirmed" do
vomit_reaction
expect do
sidekiq_perform_enqueued_jobs do
described_class.call(spam_user.id)
end
end.to change { spam_user.articles.sum(:score) }.from(0).to(-75)
end
it "lowers all of the user's articles' scores by 50 each if confirmed" do
vomit_reaction.update(status: "confirmed")
expect do
sidekiq_perform_enqueued_jobs do
described_class.call(spam_user.id)
end
end.to change { spam_user.articles.sum(:score) }.from(0).to(-150)
end
end
end