diff --git a/app/models/post_action.rb b/app/models/post_action.rb index e7dba7086..24354ca34 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -1,6 +1,5 @@ require_dependency 'rate_limiter' require_dependency 'system_message' -require_dependency 'maximum_flow' class PostAction < ActiveRecord::Base class AlreadyActed < StandardError; end @@ -387,70 +386,20 @@ class PostAction < ActiveRecord::Base def self.auto_close_if_treshold_reached(topic) return if topic.closed? - # 1) retrieve a list of pairs (user_id, post_id) representing active flags flags = PostAction.active .flags .joins(:post) .where("posts.topic_id = ?", topic.id) .where.not(user_id: Discourse::SYSTEM_USER_ID) - .pluck(:user_id, :post_id) + .group("post_actions.user_id") + .pluck("post_actions.user_id, COUNT(post_id)") - # check we have enough flags - return if flags.count < SiteSetting.num_flags_to_close_topic + # we need a minimum number of unique flaggers + return if flags.count < SiteSetting.num_flaggers_to_close_topic + # we need a minimum number of flags + return if flags.sum { |f| f[1] } < SiteSetting.num_flags_to_close_topic - # 2) build sets of unique user_ids and post_ids - user_ids = Set.new - post_ids = Set.new - - flags.each do |f| - user_ids << f[0] - post_ids << f[1] - end - - # check we have enough flaggers - return if user_ids.count < SiteSetting.num_flaggers_to_close_topic - # check we have enough posts flagged - min_post_required = SiteSetting.num_flags_to_close_topic / MAXIMUM_FLAGS_PER_POST - return if post_ids.count < min_post_required - - # 3) now we have a maximum flow problem... - # the network will have - # - edges from the 'source' to each flaggers with a capacity of '# of flags casted by that user' - # - edges for each flags with a capacity of 1 - # - edges from each posts to the 'sink' with a capacity of MAXIMUM_FLAGS_PER_POST - - # first, we need to count the # of flags casted by each users - flags_casted_by_user = {} - flags.each { |flag| flags_casted_by_user[flag[0]] = flags.count { |f| f[0] == flag[0] } } - - # then, we need to build a list of all the vertices - # ('source' being the first and 'sink" being the last) - index_of_user_id = {} - index_of_post_id = {} - - user_ids.each_with_index { |user_id, index| index_of_user_id[user_id] = 1 + index } - post_ids.each_with_index { |post_id, index| index_of_post_id[post_id] = 1 + index + user_ids.count } - - source = 0 - sink = user_ids.count + post_ids.count + 1 - n = sink + 1 - - # then, we need to build a map of all the edges (with their respective capacity) - # initially, everything is 0 (ie. no edge) - capacities = Array.new(n) { Array.new(n, 0) } - - # from the 'source' -> all user_ids with a capacity of '# of flags casted by that user' - user_ids.each { |user_id| capacities[source][index_of_user_id[user_id]] = flags_casted_by_user[user_id] } - # for each pair (user_id, post_id) with a capacity of 1 - flags.each { |f| capacities[index_of_user_id[f[0]]][index_of_post_id[f[1]]] = 1 } - # from each post_ids -> sink with a capacity of MAXIMUM_FLAGS_PER_POST - index_of_post_id.values.each { |i| capacities[i][sink] = MAXIMUM_FLAGS_PER_POST } - - # finally, we use the 'relabel to front' algorithm to solve the maximum flow problem - maximum_flow = MaximumFlow.new.relabel_to_front(capacities, source, sink) - return if maximum_flow < SiteSetting.num_flags_to_close_topic - - # 4) the threshold has been reached, we will close the topic waiting for intervention + # the threshold has been reached, we will close the topic waiting for intervention message = I18n.t("temporarily_closed_due_to_flags") topic.update_status("closed", true, Discourse.system_user, message) end diff --git a/lib/maximum_flow.rb b/lib/maximum_flow.rb deleted file mode 100644 index 0623de94a..000000000 --- a/lib/maximum_flow.rb +++ /dev/null @@ -1,69 +0,0 @@ -# cf. http://en.wikipedia.org/wiki/Maximum_flow_problem -class MaximumFlow - - # cf. http://en.wikipedia.org/wiki/Push%E2%80%93relabel_maximum_flow_algorithm - def relabel_to_front(capacities, source, sink) - n = capacities.length - flow = Array.new(n) { Array.new(n, 0) } - height = Array.new(n, 0) - excess = Array.new(n, 0) - seen = Array.new(n, 0) - queue = (0...n).select { |i| i != source && i != sink }.to_a - - height[source] = n - 1 - excess[source] = Float::INFINITY - (0...n).each { |v| push(source, v, capacities, flow, excess) } - - p = 0 - while p < queue.length - u = queue[p] - h = height[u] - discharge(u, capacities, flow, excess, seen, height, n) - if height[u] > h - queue.unshift(queue.delete_at(p)) - p = 0 - else - p += 1 - end - end - - flow[source].reduce(:+) - end - - private - - def push(u, v, capacities, flow, excess) - residual_capacity = capacities[u][v] - flow[u][v] - send = [excess[u], residual_capacity].min - flow[u][v] += send - flow[v][u] -= send - excess[u] -= send - excess[v] += send - end - - def discharge(u, capacities, flow, excess, seen, height, n) - while excess[u] > 0 - if seen[u] < n - v = seen[u] - if capacities[u][v] - flow[u][v] > 0 && height[u] > height[v] - push(u, v, capacities, flow, excess) - else - seen[u] += 1 - end - else - relabel(u, capacities, flow, height, n) - seen[u] = 0 - end - end - end - - def relabel(u, capacities, flow, height, n) - min_height = Float::INFINITY - (0...n).each do |v| - if capacities[u][v] - flow[u][v] > 0 - min_height = [min_height, height[v]].min - height[u] = min_height + 1 - end - end - end -end diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb index 13fe082f2..f32b39198 100644 --- a/spec/models/post_action_spec.rb +++ b/spec/models/post_action_spec.rb @@ -372,23 +372,20 @@ describe PostAction do it "will automatically close a topic due to large community flagging" do SiteSetting.stubs(:flags_required_to_hide_post).returns(0) - SiteSetting.stubs(:num_flags_to_close_topic).returns(12) - SiteSetting.stubs(:num_flaggers_to_close_topic).returns(5) + + SiteSetting.stubs(:num_flags_to_close_topic).returns(3) + SiteSetting.stubs(:num_flaggers_to_close_topic).returns(2) topic = Fabricate(:topic) post1 = create_post(topic: topic) post2 = create_post(topic: topic) post3 = create_post(topic: topic) - post4 = create_post(topic: topic) flagger1 = Fabricate(:user) flagger2 = Fabricate(:user) - flagger3 = Fabricate(:user) - flagger4 = Fabricate(:user) - flagger5 = Fabricate(:user) # reaching `num_flaggers_to_close_topic` isn't enough - [flagger1, flagger2, flagger3, flagger4, flagger5].each do |flagger| + [flagger1, flagger2].each do |flagger| PostAction.act(flagger, post1, PostActionType.types[:inappropriate]) end @@ -398,20 +395,18 @@ describe PostAction do PostAction.where(post: post1).delete_all # reaching `num_flags_to_close_topic` isn't enough - [flagger1, flagger2, flagger3].each do |flagger| - [post1, post2, post3, post4].each do |post| - PostAction.act(flagger, post, PostActionType.types[:inappropriate]) - end + [post1, post2, post3].each do |post| + PostAction.act(flagger1, post, PostActionType.types[:inappropriate]) end topic.reload.closed.should == false # clean up - PostAction.where(post: [post1, post2, post3, post4]).delete_all + PostAction.where(post: [post1, post2, post3]).delete_all # reaching both should close the topic - [flagger1, flagger2, flagger3, flagger4, flagger5].each do |flagger| - [post1, post2, post3, post4].each do |post| + [flagger1, flagger2].each do |flagger| + [post1, post2, post3].each do |post| PostAction.act(flagger, post, PostActionType.types[:inappropriate]) end end