require 'ostruct' module FlagQuery def self.plugin_post_custom_fields @plugin_post_custom_fields ||= {} end # Allow plugins to add custom fields to the flag views def self.register_plugin_post_custom_field(field, plugin) plugin_post_custom_fields[field] = plugin end def self.flagged_posts_report(current_user, opts = nil) opts ||= {} offset = opts[:offset] || 0 per_page = opts[:per_page] || 25 actions = flagged_post_actions(opts) guardian = Guardian.new(current_user) if !guardian.is_admin? actions = actions.where( 'category_id IN (:allowed_category_ids) OR archetype = :private_message', allowed_category_ids: guardian.allowed_category_ids, private_message: Archetype.private_message ) end total_rows = actions.count post_ids_relation = actions.limit(per_page) .offset(offset) .group(:post_id) .order('MIN(post_actions.created_at) DESC') if opts[:filter] != "old" && SiteSetting.min_flags_staff_visibility > 1 post_ids_relation = post_ids_relation.having("count(*) >= ?", SiteSetting.min_flags_staff_visibility) end post_ids = post_ids_relation.pluck(:post_id).uniq posts = DB.query(<<~SQL, post_ids: post_ids) SELECT p.id, p.cooked as excerpt, p.raw, p.user_id, p.topic_id, p.post_number, p.reply_count, p.hidden, p.deleted_at, p.user_deleted, NULL as post_actions, NULL as post_action_ids, (SELECT created_at FROM post_revisions WHERE post_id = p.id AND user_id = p.user_id ORDER BY created_at DESC LIMIT 1) AS last_revised_at, (SELECT COUNT(*) FROM post_actions WHERE (disagreed_at IS NOT NULL OR agreed_at IS NOT NULL OR deferred_at IS NOT NULL) AND post_id = p.id)::int AS previous_flags_count FROM posts p WHERE p.id in (:post_ids) SQL post_lookup = {} user_ids = Set.new topic_ids = Set.new posts.each do |p| user_ids << p.user_id topic_ids << p.topic_id p.excerpt = Post.excerpt(p.excerpt) post_lookup[p.id] = p end post_actions = actions.order('post_actions.created_at DESC') .includes(related_post: { topic: { ordered_posts: :user } }) .where(post_id: post_ids) all_post_actions = [] post_actions.each do |pa| post = post_lookup[pa.post_id] if opts[:rest_api] post.post_action_ids ||= [] else post.post_actions ||= [] end # TODO: add serializer so we can skip this action = { id: pa.id, post_id: pa.post_id, user_id: pa.user_id, post_action_type_id: pa.post_action_type_id, created_at: pa.created_at, disposed_by_id: pa.disposed_by_id, disposed_at: pa.disposed_at, disposition: pa.disposition, related_post_id: pa.related_post_id, targets_topic: pa.targets_topic, staff_took_action: pa.staff_took_action } action[:name_key] = PostActionType.types.key(pa.post_action_type_id) if pa.related_post && pa.related_post.topic conversation = {} related_topic = pa.related_post.topic if response = related_topic.ordered_posts[0] conversation[:response] = { excerpt: excerpt(response.cooked), user_id: response.user_id } user_ids << response.user_id if reply = related_topic.ordered_posts[1] conversation[:reply] = { excerpt: excerpt(reply.cooked), user_id: reply.user_id } user_ids << reply.user_id conversation[:has_more] = related_topic.posts_count > 2 end end action.merge!(permalink: related_topic.relative_url, conversation: conversation) end if opts[:rest_api] post.post_action_ids << action[:id] all_post_actions << action else post.post_actions << action end user_ids << pa.user_id user_ids << pa.disposed_by_id if pa.disposed_by_id end post_custom_field_names = [] plugin_post_custom_fields.each do |field, plugin| post_custom_field_names << field if plugin.enabled? end post_custom_fields = {} if post_custom_field_names.present? PostCustomField.where(post_id: post_ids, name: post_custom_field_names).each do |f| post_custom_fields[f.post_id] ||= {} post_custom_fields[f.post_id][f.name] = f.value end end # maintain order posts = post_ids.map { |id| post_lookup[id] } # TODO: add serializer so we can skip this posts.map! do |post| result = post.to_h if cfs = post_custom_fields[post.id] result[:custom_fields] = cfs end result end users = User.includes(:user_stat).where(id: user_ids.to_a).to_a User.preload_custom_fields(users, User.whitelisted_user_custom_fields(guardian)) [ posts, Topic.with_deleted.where(id: topic_ids.to_a).to_a, users, all_post_actions, total_rows ] end def self.flagged_post_actions(opts = nil) opts ||= {} post_actions = PostAction.flags .joins("INNER JOIN posts ON posts.id = post_actions.post_id") .joins("INNER JOIN topics ON topics.id = posts.topic_id") .joins("LEFT JOIN users ON users.id = posts.user_id") .where("posts.user_id > 0") if opts[:topic_id] post_actions = post_actions.where("topics.id = ?", opts[:topic_id]) end if opts[:user_id] post_actions = post_actions.where("posts.user_id = ?", opts[:user_id]) end if opts[:filter] == 'without_custom' return post_actions.where( 'post_action_type_id' => PostActionType.flag_types_without_custom.values ) end if opts[:filter] == "old" post_actions.where("post_actions.disagreed_at IS NOT NULL OR post_actions.deferred_at IS NOT NULL OR post_actions.agreed_at IS NOT NULL") else post_actions.active .where("posts.deleted_at" => nil) .where("topics.deleted_at" => nil) end end def self.flagged_topics results = PostAction .flags .active .includes(post: [:user, :topic]) .references(:post) .where("posts.user_id > 0") .order('post_actions.created_at DESC') ft_by_id = {} users_by_id = {} topics_by_id = {} counts_by_post = {} results.each do |pa| if pa.post.present? && pa.post.topic.present? topic_id = pa.post.topic.id ft = ft_by_id[topic_id] ||= OpenStruct.new( topic: pa.post.topic, flag_counts: {}, user_ids: [], last_flag_at: pa.created_at, meets_minimum: false ) counts_by_post[pa.post.id] ||= 0 sum = counts_by_post[pa.post.id] += 1 ft.meets_minimum = true if sum >= SiteSetting.min_flags_staff_visibility topics_by_id[topic_id] = pa.post.topic ft.flag_counts[pa.post_action_type_id] ||= 0 ft.flag_counts[pa.post_action_type_id] += 1 ft.user_ids << pa.post.user_id ft.user_ids.uniq! users_by_id[pa.post.user_id] ||= pa.post.user end end flagged_topics = ft_by_id.values.select { |ft| ft.meets_minimum } Topic.preload_custom_fields(topics_by_id.values, TopicList.preloaded_custom_fields) { flagged_topics: flagged_topics, users: users_by_id.values } end private def self.excerpt(cooked) excerpt = Post.excerpt(cooked, 200, keep_emoji_images: true) # remove the first link if it's the first node fragment = Nokogiri::HTML.fragment(excerpt) if fragment.children.first == fragment.css("a:first").first && fragment.children.first fragment.children.first.remove end fragment.to_html.strip end end