text:
# frozen_string_literal: true

module PostConcerns::Autoflagging
  extend ActiveSupport::Concern

  included do
    scope(:autoflagged, -> { where(autoflagged: true) })
    scope(:not_autoflagged, -> { where(autoflagged: false) })

    def autoflag
      Rails.logger.warn "[autoflagging] #{id}: Post#autoflag begin"

      post = self
      begin
        Rails.logger.warn "[autoflagging] #{id}: before revision count"
        post.fetch_revision_count
        unless post.revision_count == 1
          Rails.logger.warn "[autoflagging] #{id}: vandalized"
          post.send_not_autoflagged
          return 'More than one revision'
        end

        return 'Duplicate post' unless Post.where(link: link).count == 1
        return 'Flagging disabled' unless FlagSetting['flagging_enabled'] == '1'
        Rails.logger.warn "[autoflagging] #{id}: not a dupe"

        dry_run = FlagSetting['dry_run'] == '1'

        conditions = post.site.flag_conditions.where(flags_enabled: true)
        available_user_ids = {}
        conditions.each do |condition|
          if condition.validate!(post)
            available_user_ids[condition.user.id] = condition
          end
        end
        Rails.logger.warn "[autoflagging] #{id}: fetched conditions (#{conditions.to_a.size}), #{available_user_ids.size} valid (available users)"

        uids = post.site.user_site_settings.where(user_id: available_user_ids.keys).map(&:user_id)
        users = User.where(id: uids, flags_enabled: true, write_authenticated: true)
        if users.blank?
          Rails.logger.warn "[autoflagging] #{id}: no users available (#{uids.size} uids, #{users.to_a.size} users)"
          post.send_not_autoflagged
          return 'No users eligible to flag'
        end

        Rails.logger.warn "[autoflagging] #{id}: lottery begin"

        # Defined by the scaled_max_flags FlagSetting
        # scaled_max_flags = 0,0,0,99.9,99.99,101 would always allow 3 flags,
        # 4 flags on 99.9% accuracy, and 5 flags on 99.99% accuracy.
        # 101% means six flags is never allowed
        scaled_maxes = FlagSetting['scaled_max_flags']&.split(',')
        Rails.logger.warn "[autoflagging] #{id}: scaled maxes: #{scaled_maxes}"
        if !scaled_maxes.nil? && scaled_maxes.count == 6
          # Check historical accuracy
          fake_flag_condition = FlagCondition.new(
            sites: Site.mains,
            max_poster_rep: post.user_reputation,
            min_reason_count: 1,
            min_weight: post.reasons.sum(&:weight)
          )

          pre = DateTime.now
          Rails.logger.warn "[autoflagging] #{id}: check historical accuracy"
          accuracy = fake_flag_condition.accuracy # Decimal number, like 99.8
          post_ts = DateTime.now
          Rails.logger.warn "[autoflagging] #{id}: historical accuracy #{accuracy}, " \
                            "time taken #{((post_ts - pre) * 86_400).to_f} seconds"

          # If the accuracy is higher than all 6 thresholds (indicating 6 flags), index will be null
          scaled_max = scaled_maxes.index { |n| n.to_f > accuracy } || FlagSetting['max_flags'].to_i
        else
          scaled_max = FlagSetting['max_flags'].to_i
        end

        max_flags = [post.site.max_flags_per_post, (FlagSetting['max_flags'] || '3').to_i, scaled_max].min

        # Send the first flag with Smokey's account; shows up nicely in the flag queue / timeline
        # At this point, we know that at least one user has a matching flag_condition (thus 99.x% accuracy).
        # For all of the below, we're only considering users with matching flagging conditions and preferences.

        max_flags -= post.send_autoflag(User.smokey, dry_run, nil) unless User.smokey.nil?

        # The logic to select users for autoflags gives some priority to users with the Core
        # role.  The reason for this is that users with the Core role *tend* to be more
        # available to handle issues like getting FP feedback on an autoflagged post.  Core
        # users also look at it as a perk, even though it's really a responsibility.
        # For 2 flags, this will be 1 for SmokeDetector, 1 for Core users, and 0 for "non-Core" users.
        # For 3 flags, this will be 1 for SmokeDetector, 1 for Core users, and 1 for "non-Core" users.
        # For 4 flags, this will be 1 for SmokeDetector, 2 for Core users, and 1 for "non-Core" users.
        # For 5 flags, this will be 1 for SmokeDetector, 2 for Core users, and 2 for "non-Core" users.
        # For 6 flags, this will be 1 for SmokeDetector, 3 for Core users, and 2 for "non-Core" users.
        # In all of the above cases, "non-Core" means all users without the Core role and those users
        # with the Core role who were not selected for the flags allocated to Core users. How those
        # "non-Core" flags are allocated is determined below.
        core_count = (max_flags / 2.0).ceil
        other_count = max_flags - core_count
        core_users = users.with_role(:core)
        non_core_users = users.without_role(:core)
        core_users_per_flag = core_count == 0 ? 0 : core_users.length / core_count
        non_core_users_per_flag = other_count == 0 ? 0 : non_core_users.length / other_count

        core_users_used = []
        Rails.logger.warn "[autoflagging] #{id}: core..."
        core_users.shuffle.each do |user|
          break if core_count <= 0
          core_count -= post.send_autoflag(user, dry_run, available_user_ids[user.id])
          core_users_used << user
        end

        unused_core_users = core_users - core_users_used
        # It's possible here that there weren't enough core users to consume all of the core flags.
        # If so, that means that there are no unused_core_users, but that unused_core_users is empty
        # isn't something which we need to specifically handle.
        other_count += core_count

        # We don't want it to be more likely that a non-Core user with aggressive flagging
        # conditions will get flags than that the same user would get flags if they have the
        # Core role.  So, if there's more core_users per flag allocated to Core users than
        # there are non_core_users per flag allocated to non-Core users, then we share the
        # remaining flags equally between all remaining users.  If that conditions isn't
        # met, then we try the actual non-Core users first and then move on to any Core
        # users which have not already been attemped, should it not be possible to raise all
        # the desired flags with the actual non-Core users.
        remaining_users = if core_users_per_flag >= non_core_users_per_flag
                            # Equally share remaining flags between core and non-core users.
                            (non_core_users + unused_core_users).shuffle
                          else
                            # Give non-core users priority for the non-core flags.
                            # Go through all non-core users first; then add core users at the end. See #146
                            (non_core_users.shuffle + unused_core_users.shuffle)
                          end

        Rails.logger.warn "[autoflagging] #{id}: plebs..."
        remaining_users.each do |user|
          break if other_count <= 0
          other_count -= post.send_autoflag(user, dry_run, available_user_ids[user.id])
        end
      rescue => e
        Rails.logger.warn "[autoflagging] #{id}: exception #{e} :("
        FlagLog.create(success: false, error_message: "#{e}: #{e.message} | #{e.backtrace.join("\n")}",
                       is_dry_run: dry_run, flag_condition: nil, post: post,
                       site_id: post.site_id)

        # Re-raise if we're in test, 'cause it shouldn't be throwing in test
        raise if Rails.env.test?
      end

      if post.flag_logs.where(success: true).empty?
        post.send_not_autoflagged
      else
        post.update_columns(autoflagged: true)
      end
    end

    def send_autoflag(user, dry_run, condition)
      Rails.logger.warn "[autoflagging] #{id}: send_autoflag begin: #{user.username}"
      user_site_flag_count = user.flag_logs.where(site: site, success: true, is_dry_run: false).where(created_at: Date.today..Time.now).count
      return 0 if user_site_flag_count >= (user.user_site_settings.includes(:sites).where(sites: { id: site.id }).minimum(:max_flags) || -1)
      Rails.logger.warn "[autoflagging] #{id}: has enough flags"
      last_log = FlagLog.auto.where(user: user).last
      if last_log.try(:backoff).present? && (last_log.created_at + last_log.backoff.seconds > Time.now)
        Rails.logger.warn "[autoflagging] #{id}: flag settings backoff..."
        sleep((last_log.created_at + last_log.backoff.seconds) - Time.now)
      end

      Rails.logger.warn "[autoflagging] #{id}: pre spam_flag"
      success, message = user.spam_flag(self, dry_run)
      Rails.logger.warn "[autoflagging] #{id}: post spam_flag"
      backoff = 0
      backoff = message if success

      unless [
        'Flag options not present',
        'Flag option not present',
        'You do not have permission to flag this post',
        'No account on this site.',
        'User is a moderator on this site'
      ].include? message
        flag_log = FlagLog.create(success: success, error_message: message,
                                  is_dry_run: dry_run, flag_condition: condition,
                                  user: user, post: self, backoff: backoff,
                                  site_id: site_id)

        if success
          Rails.logger.warn "[autoflagging] #{id}: send_autoflagged..."
          log_as_json = JSON.parse(FlagLogController.render(locals: { flag_log: flag_log }, partial: 'flag_log.json'))
          ActionCable.server.broadcast 'api_flag_logs', flag_log: log_as_json
          ActionCable.server.broadcast 'flag_logs', row: FlagLogController.render(locals: { log: flag_log, render_source: :controller },
                                                                                partial: 'flag_log')
          Rails.logger.warn "[autoflagging] #{id}: broadcast"
        end
      end

      success ? 1 : 0
    end

    def send_not_autoflagged
      Rails.logger.warn "[autoflagging] #{id}: send_not_autoflagged..."
      ActionCable.server.broadcast 'api_flag_logs', not_flagged: {
        post_link: link,
        post: JSON.parse(PostsController.render(locals: { post: self }, partial: 'post.json'))
      }
      Rails.logger.warn "[autoflagging] #{id}: broadcast"
    end

    def eligible_flaggers
      conditions = site.flag_conditions.where(flags_enabled: true)
      available_user_ids = {}
      conditions.each do |condition|
        if condition.validate!(self)
          available_user_ids[condition.user_id] = condition
        end
      end

      uids = site.user_site_settings.where(user_id: available_user_ids.keys).map(&:user_id)
      User.where(id: uids, flags_enabled: true, write_authenticated: true)
    end

    def flagged?
      if flag_logs.loaded?
        flag_logs.select { |f| f.success && f.is_auto }.present?
      else
        flag_logs.where(success: true, is_auto: true).present?
      end
    end

    def flaggers
      User.joins(:flag_logs).where(flag_logs: { success: true, post_id: id, is_auto: true })
    end

    def manual_flaggers
      User.joins(:flag_logs).where(flag_logs: { success: true, post_id: id, is_auto: false })
    end

    def fetch_revision_count(post = nil)
      Rails.logger.warn "[autoflagging] #{id}: fetch_revision_count begin"
      post ||= self
      return if post.site.blank?
      Rails.logger.warn "[autoflagging] #{id}: site was present: #{post.site}"
      params = "key=#{AppConfig['stack_exchange']['key']}&site=#{post.site.site_domain}&filter=!mggkQI*4m9"

      url = "https://api.stackexchange.com/2.2/posts/#{post.stack_id}/revisions?#{params}"
      Rails.logger.warn "[autoflagging] #{id}: revisions URL: #{url}"
      revisions_response = Net::HTTP.get_response(URI.parse(url))
      Rails.logger.warn "[autoflagging] #{id}: revisions_response: #{revisions_response}"
      Rails.logger.warn "[autoflagging] #{id}: revisions_response: code: #{revisions_response.code}:: message: #{revisions_response.message}"
      revisions_response_body = revisions_response.body
      Rails.logger.warn "[autoflagging] #{id}: revisions_response_body: #{revisions_response_body}"
      revision_list = JSON.parse(revisions_response_body)['items']
      Rails.logger.warn "[autoflagging] #{id}: pre-select revision_list: #{revision_list}"
      # Filter out items which are not actual post revisions
      revision_list = revision_list.select { |revision| revision['revision_number'].present? }
      Rails.logger.warn "[autoflagging] #{id}: queried SE: #{revision_list&.count}"

      update(revision_count: revision_list.count)
      revision_list.count
    end

    def spam_wave_autoflag
      Thread.new do
        Rails.logger.warn '[autoflagging-sw] spam wave check begin'

        waves = SpamWave.active.joins(:sites).where(sites: { id: site_id })
        if waves.any?
          Rails.logger.warn '[autoflagging-sw] active waves exist'
          waves.each do |w|
            next unless w.post_matches?(self)
            Rails.logger.warn '[autoflagging-sw] found matching wave'
            flag_count = autoflagged? ? w.max_flags - flag_logs.successful.auto.count : w.max_flags
            users = User.where(flags_enabled: true).shuffle
            users.each do |user|
              break if flag_count <= 0
              flag_count -= send_autoflag(user, nil, nil)
            end
          end
        else
          Rails.logger.warn '[autoflagging-sw] no waves'
        end

        Rails.logger.warn '[autoflagging-sw] spam wave check end'
      end
    end
  end
end


text:
# frozen_string_literal: true

class Emails::Addressee < ApplicationRecord
  after_create do
    update(manage_key: SecureRandom.hex(32))
  end
end

repo_name: Charcoal-SE/metasmoke

repo_language: Ruby

mime_type: text/plain


text:
# frozen_string_literal: true

class Emails::Type < ApplicationRecord
  def self.[](short_name)
    find_by short_name: short_name
  end
end


text:
# frozen_string_literal: true

class Emails::Preference < ApplicationRecord
  belongs_to :type, class_name: 'Emails::Type', foreign_key: 'emails_type_id'
  belongs_to :addressee, class_name: 'Emails::Addressee', foreign_key: 'emails_addressee_id'
  belongs_to :reference, polymorphic: true

  # rubocop:disable Metrics/ParameterLists
  def self.sign_up_for_emails(email_address:, name: nil, type_short:, frequency_days:, consent_via:, consent_comment:, reference:)
    type = Emails::Type[type_short]
    if type.nil?
      throw "Unrecognized email type `#{type_short}'. Email types must be created manually before use."
    end

    addressee = Emails::Addressee.find_by(email_address: email_address)
    if addressee.nil?
      addressee = Emails::Addressee.create(name: name, email_address: email_address)
    end

    ep = Emails::Preference.create(type: type, addressee: addressee, consent_via: consent_via, consent_comment: consent_comment, enabled: true,
                                   frequency: frequency_days, next: (Date.today + frequency_days.to_i.days).to_time.iso8601, reference: reference)
    mailer = "#{type.mailer}Mailer".constantize
    mailer.send(type.method_name, ep)
  end
end


text:
# frozen_string_literal: true

class AutoflagJob < ApplicationJob
  queue_as :default

  def perform(post_id)
    post = Post.find(post_id)
    Rails.logger.warn "[autoflagging] #{post.id}: thread begin"
    # Trying to autoflag in a different thread while in test
    # can cause race conditions and segfaults. This is bad,
    # so we completely suppress the issue and just don't do that.
    post.autoflag unless Rails.env.test?
    post.spam_wave_autoflag unless Rails.env.test?
    redis.hset("posts/#{post.id}", 'flagged', post.flagged?)
  end
end


text:
# frozen_string_literal: true

class ApplicationJob < ActiveJob::Base
  # Automatically retry jobs that encountered a deadlock
  # retry_on ActiveRecord::Deadlocked

  # Most jobs are safe to ignore if the underlying records are no longer available
  # discard_on ActiveJob::DeserializationError
end


text:
# frozen_string_literal: true

class RevalidateFlagConditionsJob < ApplicationJob
  queue_as :default

  def perform
    FlagCondition.where(flags_enabled: true).find_each do |fc|
      unless fc.validate
        failures = fc.errors.full_messages
        fc.flags_enabled = false
        fc.save(validate: false)
        ActionCable.server.broadcast 'smokedetector_messages',
                                     message: "@#{fc.user&.username&.tr(' ', '')} " \
                                                "Your flag condition was disabled: #{failures.join(',')}"
      end
    end
  end
end


text:
# frozen_string_literal: true

class ValidateEligableFlaggersJob < ApplicationJob
  queue_as :default

  def perform(post)
    sys = User.find(-1)
    post.eligible_flaggers.each do |u|
      FlagCondition.validate_for_user(u, sys)
    end
  end
end


text:
# frozen_string_literal: true

class ValidateFlagConditionForUserJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    FlagCondition.validate_for_user(User.find(user_id), User.find(-1))
    # Do something later
  end
end


text:
# frozen_string_literal: true

class ScheduledMailJob < ApplicationJob
  queue_as :default

  def perform(*_args)
    Emails::Preference.joins(:addressee, :type, :reference)
                      .where(Arel.sql('next IS NOT NULL AND next <= CURRENT_TIMESTAMP AND enabled = TRUE')).each do |ep|
      mailer = "#{ep.type.mailer}Mailer".constantize
      mailer.send(ep.type.method_name, ep)
    end
  end
end


text:
# frozen_string_literal: true

class ConflictingFeedbackJob < ApplicationJob
  queue_as :default

  def perform
    Rails.logger.info "Job started (#{DateTime.now}, +0s)"

    conflicts = Post.where('created_at <= ?', 1.day.ago).where('is_tp = 1 AND (is_fp = 1 OR is_naa = 1)')\
                    .includes(:feedbacks, feedbacks: { user: :roles })
    Rails.logger.info "Total #{conflicts.count} existing conflicts."

    immediate_timer = Benchmark.measure do
      immediate_resolution_data = conflicts.map do |p|
        feedback_classes = p.feedbacks.map { |f| f.feedback_type[0] }
        unique_classes = feedback_classes.uniq
        feedback_counts = unique_classes.map { |fc| [fc, feedback_classes.count(fc)] }.to_h
        [p, feedback_counts]
      end.to_h

      # Magic number: minimum threshold to be immediately resolvable. One feedback class must outstrip another
      # by this amount for the conflict to be resolvable; otherwise, it's going to need more human eyes.
      resolvable = immediate_resolution_data.select do |_post, feedback_counts|
        counts = feedback_counts.values
        counts.max >= (counts.max(2)[1] || counts.max) + 2
      end

      Rails.logger.info "Found #{resolvable.size} immediately resolvable conflicts."

      resolvable.each do |post, feedback_counts|
        winner = feedback_counts.max_by { |_k, v| v }[0]
        post.feedbacks.where('LEFT(feedback_type, 1) != ?', winner).update_all(is_invalidated: true, invalidated_by: -1, invalidated_at: DateTime.now)
      end
    end

    detailed_timer = Benchmark.measure do
      # User precedences. Higher numbers mean a user possessing this role will override a user possessing a lower role.
      # Everyone gets flagger, so that's worth a whole nothing; developer likewise because you can be a developer without knowing anything about
      # feedback (and all of our current developers have other high-precedence roles as well).
      roles = { flagger: 0, reviewer: 1, core: 2, blacklist_manager: 3, smoke_detector_runner: 3, admin: 4, developer: 0 }

      # One-liner beauty (or hell, depending how you look at it). First get a list of all feedbacks and the maximum precedence of the user who
      # created it. Secondly, take that list and uniq-ify it by summing all precedences for the same feedback type.
      detailed_resolution_data = conflicts.map do |p|
        feedback_data = p.feedbacks.map { |f| [f.feedback_type[0], (f.user&.roles&.map { |r| roles[r.name.to_sym] }&.max || 0)] }
        sums = feedback_data.map { |f| f[0] }.uniq.map { |ft| [ft, feedback_data.select { |f| f[0] == ft }.map { |f| f[1] }.sum] }
        [p, sums]
      end

      # If one feedback type has a higher total user precedence than any others, we can resolve the conflict to that feedback type.
      resolvable = detailed_resolution_data.select do |_post, feedback_data|
        values = feedback_data.map { |f| f[1] }
        values.max > (values.max(2)[1] || values.max)
      end

      Rails.logger.info "Found #{resolvable.size} further conflicts resolvable by user precedence."

      resolvable.each do |post, feedback_data|
        winner = feedback_data.max_by { |f| f[1] }[0]
        post.feedbacks.where('LEFT(feedback_type, 1) != ?', winner).update_all(is_invalidated: true, invalidated_by: -1, invalidated_at: DateTime.now)
      end
    end

    Rails.logger.info "Job finished (#{DateTime.now}, immediate=#{immediate_timer.real.round(2)}s, detailed=#{detailed_timer.real.round(2)}s)"
  end
end


text:
# frozen_string_literal: true

class FeedbackReindexJob < ApplicationJob
  queue_as :default

  def perform
    query_results = ActiveRecord::Base.connection.execute(File.read(Rails.root.join('lib/queries/incorrectly_indexed_feedback.sql').to_s)).to_a
    bad_ids = query_results.map(&:first).flatten
    reindexed_count = 0
    bad_ids.each do |pid|
      reindexed_count += Post.find(pid).update_feedback_cache ? 1 : 0
    end
    Rails.logger.info "Reindexed feedback for #{reindexed_count} posts"
  end
end


text:
# frozen_string_literal: true

require 'yaml'

class RedisLogJob < ApplicationJob
  queue_as :default
  before_enqueue do |job|
    job.arguments.map! do |i|
      begin
        i.to_yaml
      rescue
        ''
      end
    end
  end

  before_perform do |job|
    job.arguments.map! { |arg| YAML.load(arg) } # rubocop:disable Security/YAMLLoad
  end

  # rubocop:disable Metrics/ParameterLists
  def perform(root, subspaces: {}, status: nil, exception: {}, session_id: nil, user_id: nil, time:, uuid:, completed: false)
    # rubocop:enable Metrics/ParameterLists
    keyspace_map = subspaces
    @time = time
    @uuid = uuid
    @redis = redis(logger: true, new: true)
    # Because ActionJob is async, sometimes the second set of info will come before data from the first is in redis. That causes bad.
    if completed
      @redis.set("#{redis_log_key}/metastatus", Time.now.to_i, nx: true)
      if @redis.get("#{redis_log_key}/metastatus") != 'before_action' && Time.now.to_i - @redis.get("#{redis_log_key}/metastatus").to_i < 20
        retry_job wait: (1 / 2).seconds
        return
      else
        @redis.del "#{redis_log_key}/metastatus"
      end
    end
    @redis.multi
    @redis.mapped_hmset redis_log_key, root
    @redis.expire redis_log_key, REDIS_LOG_EXPIRATION
    log_to_namespace(redis_log_key, keyspace_map)
    log_exception(exception) if exception[:exception].present?
    if completed
      @redis.zrem 'requests/status/UNK', @uuid
      @redis.zadd "requests/status/#{status || 'INC'}", @time, @uuid
    else
      @redis.zadd 'requests/status/UNK', @time, @uuid
    end
    unless completed
      # These things only need to be done once
      log_session(session_id, user_id)
      @redis.zadd 'requests', @time, @uuid
    end
    @redis.set "#{redis_log_key}/metastatus", 'before_action' unless completed
    @redis.exec

    send_out_websocket(completed)
  end

  private

  def redis_log_key
    # We include the time just to doubly ensure that the uuid is unique
    "request/#{@time}/#{@uuid}"
  end

  def log_to_namespace(namespace, keyspaces)
    keyspaces.each do |subspace, hsh|
      next if hsh.empty?
      @redis.mapped_hmset "#{namespace}/#{subspace}", hsh
      @redis.expire "#{namespace}/#{subspace}", REDIS_LOG_EXPIRATION
    end
  end

  def log_session(session_id, user_id)
    return if session_id.nil?
    # TODO: If we care more about memory than speed, switch session/*/requests and user_sessions/* to be sets and
    # intersect them with requests to get a zset when you need one.
    @redis.zadd "session/#{session_id}/requests", @time, @uuid
    @redis.expire "session/#{session_id}/requests", REDIS_LOG_EXPIRATION
    @redis.hsetnx("session/#{session_id}", 'start', @time)
    @redis.hset("session/#{session_id}", 'end', @time)
    @redis.expire "session/#{session_id}", REDIS_LOG_EXPIRATION
    return unless user_id.nil?
    @redis.zadd "user_sessions/#{user_id}", @time, session_id
    @redis.expire "user_sessions/#{user_id}", REDIS_LOG_EXPIRATION * 3
  end

  def log_exception(data)
    @redis.hset redis_log_key, 'exception', data[:exception].join("\n")
    ex = data[:exception_object]
    @redis.mapped_hmset "#{redis_log_key}/exception", file_name: ex.try(:file_name),
                                                      annotated_source_code: ex.try(:annoted_source_code)&.join("\n"),
                                                      line_number: ex.try(:line_number),
                                                      backtrace: ex.try(:backtrace)&.join("\n"),
                                                      message: ex.try(:message)
    @redis.expire "#{redis_log_key}/exception", REDIS_LOG_EXPIRATION
  end

  def send_out_websocket(completed)
    info = @redis.hgetall(redis_log_key)
                 .merge(request_headers: @redis.hgetall("#{redis_log_key}/request_headers"),
                        response_headers: @redis.hgetall("#{redis_log_key}/response_headers"),
                        params: @redis.hgetall("#{redis_log_key}/params"),
                        exception: @redis.hgetall("#{redis_log_key}/exception"),
                        timestamp: @time,
                        request_id: @uuid,
                        logs: @redis.lrange("#{redis_log_key}/logs", 0, -1),
                        key: "#{@time.to_s.tr('.', '-')}-#{@uuid}")
    ws_msg = {
      key: "#{@time.to_s.tr('.', '-')}-#{@uuid}",
      precedence: completed ? 1 : 0,
      html: ApplicationController.render(
        template: 'redis_log/_row',
        locals: { req: info, wrapped: true },
        layout: nil
      )
    }
    ActionCable.server.broadcast 'redis_log_channel', **ws_msg
    {
      status: info['status'],
      user: info['user_id'],
      session: info['session_id'],
      path: Rails.sensible_routes.match_for(info['path'])&.path || info['path'].split('?').first
    }.each do |filter, value|
      ActionCable.server.broadcast "redis_log_#{filter}_#{value}", **ws_msg
    end
  end
end


text:
# frozen_string_literal: true

class CleanRedisJob < ApplicationJob
  queue_as :default

  PREFIX = 'cleanups'

  def perform
    @keys = []
    thread = Thread.new {}
    Post.eager_load(:stack_exchange_user)
        .eager_load(:site)
        .eager_load(:comments)
        .eager_load(:reasons)
        .eager_load(:flag_logs)
        .eager_load(:feedbacks)
        .find_in_batches(batch_size: 250) do |posts|
      thread.join
      thread = Thread.new do
        redis.pipelined do
          posts.each do |post|
            redis.sadd ck('tps'), post.id if post.is_tp
            redis.sadd ck('fps'), post.id if post.is_fp
            redis.sadd ck('naas'), post.id if post.is_naa
            if post.link.nil?
              redis.sadd ck('nolink'), post.id
            else
              redis.sadd ck('questions'), post.id if post.question?
              redis.sadd ck('answers'), post.id if post.answer?
            end
            redis.sadd ck('autoflagged'), post.id if post.flagged?
            redis.sadd ck("sites/#{post.site_id}/posts"), post.id
            redis.sadd ck('all_posts'), post.id
            redis.zadd ck('posts'), post.created_at.to_i, post.id
            post_hash = {
              body: post.body,
              title: post.title,
              reason_weight: post.reasons.map(&:weight).reduce(:+),
              created_at: post.created_at,
              username: post.username,
              link: post.link,
              site_site_logo: post.site.try(:site_logo),
              stack_exchange_user_username: post.stack_exchange_user.try(:username),
              stack_exchange_user_id: post.stack_exchange_user.try(:id),
              flagged: post.flagged?,
              site_id: post.site_id,
              post_comments_count: post.comments.length, # post.comments.count,
              why: post.why
            }
            redis.hmset("posts/#{post.id}", *post_hash.to_a)

            reason_names = post.reasons.map(&:reason_name)
            reason_weights = post.reasons.map(&:weight)
            redis.zadd(ck("posts/#{post.id}/reasons"), reason_weights.zip(reason_names)) unless post.reasons.empty?

            nil
          end
          nil
        end
      end
      GC.start
      nil
    end

    Feedback.eager_load(:post).eager_load(:user).eager_load(:api_key).find_each(batch_size: 10_000) do |fb|
      redis.pipelined do
        next if fb.post.nil?
        redis.sadd ck(key), fb.id
        redis.hmset "feedbacks/#{fb.id}", *{
          feedback_type: fb.feedback_type,
          username: fb.user.try(:username) || fb.user_name,
          app_name: fb.api_key.try(:app_name),
          invalidated: fb.is_invalidated
        }
      end
    end
    post_ids = ActiveRecord::Base.connection.select_all('SELECT id FROM posts').rows.flatten
    redis.scan_each(match: "#{PREFIX}/post/*/feedbacks") do |key|
      redis.del key unless post_ids.include?(key.split('/')[1].to_i)
    end
    post_ids = nil

    redis.pipelined do
      DeletionLog.eager_load(:post).find_each do |dl|
        redis.hset("posts/#{dl.post.id}", 'deleted_at', dl.created_at.to_s) if dl.is_deleted
      end
    end

    redis.pipelined do
      Reason.find_each do |i|
        redis.sadd(ck('reasons'), i.id)
        redis.sadd(ck("reasons/#{i.id}"), i.posts.select(:id).map(&:id))
      end
    end

    redis.pipelined do
      SiteSetting.find_each do |ss|
        redis.set ck("site_settings/#{ss.name}"), ss.value
      end
    end

    redis.pipelined do
      ReviewItem.eager_load(:queue).find_each do |ri|
        redis.sadd ck('review_queues'), ri.queue.id
        if ri.completed
          redis.srem ck("review_queue/#{ri.queue.id}/unreviewed"), ri.id
        else
          redis.sadd ck("review_queue/#{ri.queue.id}/unreviewed"), ri.id
        end
      end
    end

    redis.pipelined do
      ReviewQueue.find_each do |rq|
        redis.sadd ck('review_queues'), rq.id
      end
    end

    [['posts', Post], ['reasons', Reason], ['feedbacks', Feedback]].each do |str, _type|
      ids = ActiveRecord::Base.connection.select_all("SELECT id FROM #{str}").rows.flatten
      redis.scan_each(match: "#{str}/*") do |elem|
        redis.del elem unless ids.include?(elem.split('/')[1].to_i)
      end
    end

    Rails.logger.info @keys
    @keys.uniq.each do |key|
      unless redis.exists("cleanups/#{key}")
        Rails.logger.warn("WARNING: Could not copy key #{key}. " \
                          "Does cleanups/#{key} exists? #{redis.exists("cleanups/#{key}") ? 'yes' : 'no'} | " \
                          "#{key} exists? #{redis.exists(key) ? 'yes' : 'no'}")
      end
      redis.rename "cleanups/#{key}", key unless redis.exists "cleanups/#{key}"
    end
    @keys = []
  end

  private

  def ck(key)
    @keys ||= []
    @keys.push(key) unless @keys.include? key
    "#{PREFIX}/#{key}"
  end
end


text:
# frozen_string_literal: true

class RecheckReviewQueueJob < ApplicationJob
  queue_as :default

  def perform(queue)
    queue.items.includes(:reviewable).each do |i|
      i.update(completed: true) if i.reviewable.should_dq?(queue)
    end
  end
end


text:
# frozen_string_literal: true

module FlagConditionsHelper
end


text:
# frozen_string_literal: true

module PostsHelper
  def render_links(text)
    # Don't forget to escape the '.' because regex!
    permitted_sites = %w[
      (?:stackoverflow|superuser|serverfault|askubuntu|stackapps)\\.com
      mathoverflow\\.net
      \\w*.\\.stackexchange\\.com
      m(?:etasmoke)?\\.erwaysoftware\\.com
    ]
    raw(text.split(%r{((?:https?:)?\/{2}(?:#{permitted_sites.join('|')})[^\)\<\s]*)}).map.with_index do |s, i|
      i.even? ? html_escape(s) : link_to(s, s)
    end.join)
  end
end


text:
# frozen_string_literal: true

module PostCommentsHelper
end


text:
# frozen_string_literal: true

module CustomSessionsHelper
end


text:
# frozen_string_literal: true

module SpamDomainsHelper
end


text:
# frozen_string_literal: true

module SiteSettingsHelper
end


text:
# frozen_string_literal: true

module SearchHelper
  def self.parse_search_params(params, symbol, user)
    input = params[symbol] || ''

    if params[is_regex?(symbol)]
      operation = if !user.nil? && user.can_use_regex_search?
                    params[is_inverse_regex?(symbol)] ? 'NOT REGEXP' : 'REGEXP'
                  else
                    false
                  end
    else
      operation = 'LIKE'
      unless input.nil? || input.empty?
        input = '%' + ActiveRecord::Base.sanitize_sql_like(input) + '%'
      end
    end

    [input, operation]
  end

  def self.is_regex?(symbol) # rubocop:disable Style/PredicateName
    (symbol.to_s + '_is_regex').to_sym
  end

  def self.is_inverse_regex?(symbol) # rubocop:disable Style/PredicateName
    (symbol.to_s + '_is_inverse_regex').to_sym
  end
end


text:
# frozen_string_literal: true

module ReasonsHelper
  def self.check_for_inactive_reasons
    Reason.where(inactive: false).each do |reason|
      begin
        if reason.posts.last.created_at < 30.days.ago
          reason.inactive = true
          reason.save!
        end
      rescue # rubocop:disable Lint/HandleExceptions
      end
    end
  end

  def self.calculate_weights_for_flagging
    Reason.all.each do |reason|
      if reason.posts.count > 20
        reason.update(weight: [(reason.tp_percentage * 100).round, reason.maximum_weight].compact.min)
      else
        reason.update(weight: 0)
      end
    end
  end
end


text:
# frozen_string_literal: true

module DeletionLogsHelper
end


text:
# frozen_string_literal: true

module SitesHelper
  def self.update_sites
    require 'net/http'
    url = URI.parse("https://api.stackexchange.com/2.2/sites?pagesize=1000&key=#{AppConfig['stack_exchange']['key']}&filter=!SmNndiHFp*Yrs)zUme")
    res = Net::HTTP.get_response(url)
    sites = JSON.parse(res.body)['items']
    return unless sites.count > 100 # all is not well; bail
    ids = sites.map do |site|
      uri = URI.parse(site['site_url'])
      aliased_hosts = (site['aliases'] || []).map { |a| URI.parse(a).host }
      print "#{uri.host} " unless Rails.env.test?
      if Site.exists?(site_domain: uri.host)
        s = Site.find_by(site_domain: uri.host)
      elsif Site.exists?(site_domain: aliased_hosts)
        s = Site.find_by(site_domain: aliased_hosts)
        s.site_domain = uri.host
      else
        s = Site.new(site_domain: uri.host)
      end

      if s.new_record?
        puts "[\x1b[1m\x1b[36mnew\x1b[0m]" unless Rails.env.test?
        SmokeDetector.send_message_to_charcoal "New site [#{site['name']}](#{site['site_url']}) launched!" unless Rails.env.test?
      else
        puts "[\x1b[32mok\x1b[0m]" unless Rails.env.test?
      end

      s.api_parameter = site['api_site_parameter']
      s.site_url = site['site_url']
      s.site_logo = site['favicon_url'].gsub(/https?:/, '')
      s.site_name = site['name']
      s.is_child_meta = site['site_type'] == 'meta_site'
      s.save!
      s.id
    end
    Site.where.not(id: ids).update_all(closed: true)
  end

  # Not actually intended to be used in prod code, just there as a console helper.
  def self.do_site_rename(old_name, new_name)
    old_site = Site.find_by site_name: old_name
    new_site = Site.find_by site_name: new_name
    puts "Old: ##{old_site.id} '#{old_site.site_name}'; new: ##{new_site.id} '#{new_site.site_name}'"

    puts 'Remapping Posts...'
    Post.where(site_id: old_site.id).update_all(site_id: new_site.id)

    puts 'Remapping StackExchangeUsers...'
    StackExchangeUser.where(site_id: old_site.id).update_all(site_id: new_site.id)

    puts 'Remapping FlagLogs...'
    FlagLog.where(site_id: old_site.id).update_all(site_id: new_site.id)

    puts 'Removing ModeratorSites...'
    ModeratorSite.where(site_id: old_site.id).delete_all

    puts 'Remapping HABTM_FlagConditions...'
    ActiveRecord::Base.connection.execute "UPDATE flag_conditions_sites SET site_id = #{new_site.id} WHERE site_id = #{old_site.id};"

    puts 'Remapping HABTM_UserSiteSettings...'
    ActiveRecord::Base.connection.execute "UPDATE sites_user_site_settings SET site_id = #{new_site.id} WHERE site_id = #{old_site.id};"

    puts 'Removing Site...'
    old_site.destroy

    puts 'Done'
  end
end


text:
# frozen_string_literal: true

module ReviewQueuesHelper
end


text:
# frozen_string_literal: true

module FlagSettingsHelper
  def flag_settings_plot_lines(start_time, end_time = Time.now)
    setting_ids = FlagSetting.where(name: %w[max_flags min_accuracy]).pluck(:id)

    audits = Audited::Audit.where(auditable_type: 'FlagSetting')
                           .where(action: 'update')
                           .where(auditable_id: setting_ids)
                           .where(created_at: start_time..end_time)

    audits.map do |a|
      changes = a.audited_changes['value']
      { 'width' => '1', value: a.created_at.to_i * 1000, color: 'lightgrey',
        label: { text: "#{FlagSetting.find(a.auditable_id).name.humanize}: #{changes.first} → #{changes.last}" },
        'zIndex' => 10 }
    end
  end
end


text:
# frozen_string_literal: true

module FlagLogHelper
end


text:
# frozen_string_literal: true

module APIKeysHelper
end


text:
# frozen_string_literal: true

module AbuseContactsHelper
end


text:
# frozen_string_literal: true

module SpamWavesHelper
  def help_or_error(field, help = nil, &block)
    if @errors.any? && @errors.messages.include?(field)
      field.to_s.humanize + ' ' + @errors.messages[field].first
    elsif block_given?
      capture(&block)
    else
      help
    end
  end

  def error?(field)
    @errors.any? && @errors.messages.include?(field)
  end

  def default(field, wave = nil, default = nil)
    if field.to_s.include? '['
      (begin
         wave.conditions[field.to_s.split('[')[1].tr(']', '')]
       rescue
         nil
       end) || params[field] || default
    else
      (begin
         wave&.send(field)
       rescue
         nil
       end) || params[field] || default
    end
  end
end


text:
# frozen_string_literal: true

module AnnouncementsHelper
end


text:
# frozen_string_literal: true

module DomainGroupsHelper
end


text:
# frozen_string_literal: true

module CodeStatusHelper
end


text:
# frozen_string_literal: true

module DumpsHelper
end


text:
# frozen_string_literal: true

module DashboardHelper
end


text:
# frozen_string_literal: true

module APIHelper
  def authorized_get(uri, options = {})
    authorized_request(uri, :get, options)
  end

  def authorized_post(uri, options = {})
    authorized_request(uri, :post, options)
  end

  def authorized_request(uri, method, options = {})
    headers = options[:headers] || {}
    data = options[:data]
    parse_json = options[:parse_json]
    parse_json = true unless options.key? :parse_json
    options = options.clone
    options.delete :headers
    options.delete :data
    options.delete :parse_json

    potential_tokens = GithubToken.where('expires > ?', Time.now + 1.minute)
    if potential_tokens.present?
      access_token = potential_tokens.last.token
    else
      private_pem = File.read("#{Rails.root}#{AppConfig['github']['integration_pem_file']}")
      private_key = OpenSSL::PKey::RSA.new(private_pem)
      payload = {
        iat: Time.now.to_i,
        exp: 1.minute.from_now.to_i,
        iss: AppConfig['github']['integration_id']
      }
      jwt = JWT.encode(payload, private_key, 'RS256')

      token_resp = HTTParty.post("https://api.github.com/installations/#{AppConfig['github']['installation_id']}/access_tokens", headers: {
                                   'Accept' => 'application/vnd.github.machine-man-preview+json',
                                   'User-Agent' => 'metasmoke-ci/1.0',
                                   'Authorization' => "Bearer #{jwt}"
                                 })
      token_resp = JSON.parse(token_resp.body)
      GithubToken.create!(token: token_resp['token'], expires: token_resp['expires_at'])
      access_token = token_resp['token']
    end

    resp = HTTParty.send(method, uri, {
      body: data.to_json, headers: {
        'Accept' => 'application/vnd.github.machine-man-preview+json',
        'Authorization' => "token #{access_token}",
        'Content-Type' => 'application/json',
        'User-Agent' => 'metasmoke-ci/1.0'
      }.merge(headers)
    }.merge(options))
    if parse_json
      JSON.parse resp.body, symbolize_names: true
    else
      resp.body
    end
  end

  def generator_table(cls)
    field_names = cls.column_names
    raw(render('api/filter_table', table_name: cls.table_name, cols: field_names))
  end

  def filter_generator
    cols = [Announcement, APIKey, Audited::Audit, CommitStatus, DeletionLog, DomainTag, Feedback, FlagCondition, FlagLog,
            FlagSetting, ModeratorSite, Post, Reason, Role, Site, SmokeDetector, SpamDomain, StackExchangeUser, Statistic,
            UserSiteSetting, User, PostComment].map do |cls|
      generator_table cls
    end
    raw(cols.each_slice(3).map do |g|
      "
#{g.join("\n")}
"
    end.join("\n"))
  end

  def self.filter_config
    available = [Announcement, APIKey, Audited::Audit, CommitStatus, DeletionLog, DomainTag, Feedback, FlagCondition, FlagLog,
                 FlagSetting, ModeratorSite, Post, Reason, Role, Site, SmokeDetector, SpamDomain, StackExchangeUser, Statistic,
                 UserSiteSetting, User, PostComment].map do |cls|
      cls.column_names.map { |cn| "#{cls.table_name}.#{cn}" }
    end.flatten - AppConfig['sensitive_fields']
    available.to_yaml
  end
end


text:
# frozen_string_literal: true

module AuthenticationHelper
  def auth_url(scope, redirect_uri)
    config = AppConfig['stack_exchange']
    "https://stackexchange.com/oauth?client_id=#{config['client_id']}&scope=#{scope}&redirect_uri=#{redirect_uri}"
  end

  def write_auth_url
    config = AppConfig['token_store']
    state = Rails.cache.fetch("token_migration_state/#{current_user.id}", expires_in: 30.minutes) do
      SecureRandom.hex(10)
    end
    "#{config['host']}/auth?state=#{state}"
  end

  def identify_auth_url
    config = AppConfig['stack_exchange']
    auth_url('', config['redirect_uri'])
  end

  def login_auth_url
    config = AppConfig['stack_exchange']
    auth_url('', config['login_redirect_uri'])
  end

  def signup_auth_url
    login_auth_url
  end

  def access_token_from_code(_code, redirect_uri = AppConfig['stack_exchange']['redirect_uri'])
    config = AppConfig['stack_exchange']

    request_params = {
      client_id: config['client_id'],
      client_secret: config['client_secret'],
      code: params[:code],
      redirect_uri: redirect_uri
    }.stringify_keys
    response = Rack::Utils.parse_nested_query(Net::HTTP.post_form(URI.parse('https://stackexchange.com/oauth/access_token'), request_params).body)

    response['access_token']
  end

  def info_for_access_token(access_token)
    config = AppConfig['stack_exchange']
    response = open("https://api.stackexchange.com/2.2/access-tokens/#{access_token}?key=#{config['key']}").read
    begin
      JSON.parse(response)['items'][0]
    rescue OpenURI::HTTPError
      ap response
      raise
    end
  end
end


text:
# frozen_string_literal: true

module AbuseReportsHelper
end


text:
# frozen_string_literal: true

module AbuseCommentsHelper
end


text:
# frozen_string_literal: true

module NumberHelper
  def number_to_multiplicative_quantifier(n)
    return 'once' if n == 1
    return 'twice' if n == 2

    "#{n} times"
  end
end


text:
# frozen_string_literal: true

module StatisticsHelper
end


text:
# frozen_string_literal: true

module Filterator
  class V1
    # I'm fairly sure most of this class works by magic, rather than by any design.

    # This thing turns strings of characters into strings of binary digits, where the binary digits come from the
    # character ordinals of each character in the string.
    # For each character, convert its ordinal to binary, pad the result with 0's so it's 8 digits long, and append the
    # 8 digits generated to an array; that array gets joined into one long string, which is what actually gets stored in
    # this hash.
    @generator = Hash.new do |h, k|
      chars = k.chars
      mappings = AppConfig['api_field_mappings']
      h[k] = chars.map.with_index do |c, i|
        if i < chars.size - 1
          c.ord.to_s(2).rjust(8, '0')
        else
          rem = mappings.size % 8
          rem = 8 if rem == 0
          c.ord.to_s(2).rjust(rem, '0')
        end
      end.join('')
    end

    # This takes a Base64-encoded filter string and turns it into an array of database-level qualified database column
    # names.
    # Uses @generator to turn the decoded Base64 into a binary string; turns the string into an array of 0's and 1's,
    # and zips the result together with the API field mappings in config. Selects only the fields that now have a 1 next
    # to them, and returns the resulting list of field names.
    def self.fields_from_string(encoded)
      raw = Base64.decode64(encoded)
      bitstring = @generator[raw]
      bits = bitstring.chars.map(&:to_i)
      AppConfig['api_field_mappings'].zip(bits).map { |k, v| k if v == 1 }.compact
    end

    # This is precisely the opposite of the previous method: given a list of database-level qualified column names, turns
    # them into a Base64-encoded filter string.
    # Creates an array the length of the API field mappings in config, and populates it - for each index, if the field at
    # that index in the mappings is present in the list of fields passed to this method, the array will be populated with
    # a 1; 0 if not. Chops the array into groups of 8, parses each group of 8 as a binary number to turn it to decimal,
    # and then turns the decimals into their respective characters. Finally, Base64 encodes the lot and returns it.
    def self.string_from_fields(fields)
      mappings = AppConfig['api_field_mappings']
      bits = Array.new(mappings.size) do |i|
        fields.include?(mappings[i]) ? 1 : 0
      end
      raw = bits.each_slice(8).map { |x| x.join('').to_i(2).chr }.join('')
      Base64.encode64(raw)
    end
  end

  class V2
    # This class is slightly less magic than V1.
    # Filters look like this: HJHGJJKKIIKKNMGOJILKKMMJFLOOKIGNONNMGK
    # TL;DR: It's a binary bitfield, turned into a decimal integer, and each digit is turned into a character.

    def self.filter_from_fields(fields)
      mappings = AppConfig['api_field_mappings']
      bits = Array.new(mappings.size) do |i|
        fields.include?(mappings[i]) ? 1 : 0
      end
      as_int = bits.join.to_i(2)
      as_int.to_s.chars.map { |c| (c.to_i + 70).chr }.join
    end

    def self.fields_from_filter(filter)
      as_int = filter.chars.map { |c| c.ord - 70 }.join.to_i
      bits = as_int.to_s(2).rjust(AppConfig['api_field_mappings'].size, '0').chars.map(&:to_i)
      AppConfig['api_field_mappings'].zip(bits).map { |k, v| k if v == 1 }.compact - AppConfig['sensitive_fields']
    end
  end
end


text:
# frozen_string_literal: true

module FeedbacksHelper
  def element_class_for_feedback(f)
    if f.is_negative?
      'text-danger'
    elsif f.is_positive?
      'text-success'
    else
      ''
    end
  end

  def element_symbol_for_feedback(f)
    if f.is_negative?
      '✗'
    elsif f.is_positive?
      '✓'
    elsif f.is_naa?
      '💩'
    elsif f.feedback_type.downcase.include? 'ignore'
      '🚫'
    else
      ''
    end
  end
end


text:
# frozen_string_literal: true

include ERB::Util

module ApplicationHelper
  def title(text)
    content_for :title, text
    text
  end

  def render_markdown(text)
    CommonMarker.render_doc(text.present? ? text : "Markdown wasn't present",
                            %i[LIBERAL_HTML_TAG],
                            %i[autolink tagfilter]).to_html(:UNSAFE)
  end

  def safe_render_markdown(text, options = {})
    scrubber = options[:scrubber] || PostScrubber.new
    raw(sanitize(render_markdown(text), scrubber: scrubber))
  end

  @current_dropdown_is_active = nil
  def nav_link(cls, options = {}, &block)
    Rack::MiniProfiler.step("Generating nav_link: #{cls}") do
      options[:active] ||= []
      options[:attrs] ||= {}
      options[:attrs][:class] ||= []
      options[:action] ||= :index
      cls ||= options[:controller]

      if cls
        controller_name = cls.name.underscore.sub(/_controller$/, '')
        if options[:label].nil?
          options[:label] = if options[:action] == :index
                              controller_name
                            else
                              options[:action]
                            end
        end
        allowed_keys = %w[action anchor only_path].map(&:to_sym)
        url = url_for({
          # needs to be /#{controller_name} because otherwise, when you’re in a
          # scoped controller (e.g. devise/*), it looks for `devise/#{controller_name}`
          controller: "/#{controller_name}"
        }.merge(options.select { |key| allowed_keys.include? key })
         .merge(options[:params] || {}))
      elsif options[:path].present?
        url = options[:path]
      else
        url = '#'
      end

      options[:label] = options[:label].to_s
      options[:label] = options[:label].sub 'Smoke Detector', 'SmokeDetector' unless @current_dropdown_is_active.nil?

      link = if block_given?
               link_to(url, options[:link_attrs]) { h(options[:label]) + ' ' + capture(&block) }
             else
               link_to options[:label], url, options[:link_attrs]
             end

      if [true, false].include? options[:active]
        is_active = options[:active]
      else
        actives = options[:active]
                  .clone
                  .unshift([cls, options[:action]])
                  .reject { |item| item.nil? || (item.is_a?(Array) && item[0].nil?) }
                  .map { |item| item.is_a?(Array) ? item : [item, nil] }

        is_active = !actives.select { |args| current_action?(*args) }.empty?
      end

      @current_dropdown_is_active ||= is_active unless @current_dropdown_is_active.nil?

      tag.li link + options[:children], options[:attrs].merge(class: [is_active ? 'active' : '', *options[:attrs][:class]])
    end
  end

  def nav_dropdown(cls = nil, options = {}, &block)
    if cls.is_a? Hash
      options = cls
      cls = nil
    end

    @current_dropdown_is_active = false
    children = capture(&block)
    is_active = @current_dropdown_is_active
    @current_dropdown_is_active = nil

    nav_link(
      cls,
      options.merge(
        active: !!is_active, # rubocop:disable Style/DoubleNegation
        attrs: {
          class: 'dropdown'
        },
        link_attrs: {
          class: 'dropdown-toggle',
          data: {
            toggle: 'dropdown'
          },
          role: 'button',
          aria: {
            haspopup: true,
            expanded: false
          }
        },
        children: tag.ul(children, class: 'dropdown-menu')
      )
    ) { tag.span class: 'caret' }
  end

  protected

  def current_action?(cls, action = nil)
    return false unless controller.is_a? cls
    return controller.action_name == action.to_s if action
    true
  end
end


text:
# frozen_string_literal: true

module AbuseReportStatusesHelper
end


text:
# frozen_string_literal: true

module MicroAuthHelper
end


text:
# frozen_string_literal: true

module DeveloperHelper
end


text:
# frozen_string_literal: true

module DataHelper
end


text:
# frozen_string_literal: true

module RSSHelper
end


text:
# frozen_string_literal: true

module CustomRegistrationsHelper
end


text:
# frozen_string_literal: true

module GraphsHelper
end


text:
# frozen_string_literal: true

module SmokeDetectorsHelper
  def self.escape_markdown(s)
    %w{[ ] * _ `}.each do |token|
      s = s.gsub token, '\\' + token
    end
    s
  end
end


text:
# frozen_string_literal: true

module DomainTagsHelper
end


text:
# frozen_string_literal: true

module UserSiteSettingsHelper
end


text:
# frozen_string_literal: true

module DomainLinksHelper
end


text:
# frozen_string_literal: true

module RedisLogHelper
end


text:
# frozen_string_literal: true

module Emails::PreferencesHelper
end


text:
# frozen_string_literal: true

class StatusChannel < ApplicationCable::Channel
  def subscribed
    if current_user&.has_role?(:blacklist_manager)
      stream_from 'status_blacklist_manager'
    else
      stream_from 'status'
    end
  end
end


text:
# frozen_string_literal: true

# If you change this to APIChannel, FIRE and AIM will break!
class ApiChannel < ApplicationCable::Channel
  def subscribed
    @key = APIKey.find_by(key: params[:key])
    if params[:key].present? && @key.present?
      if params[:events].present?
        params[:events].split(';').each do |e|
          if e.include? '#'
            event_types = e.split('#')[1].split(',')
            e = e.split('#')[0]
          else
            event_types = %w[create update]
          end
          event_types.each do |et|
            stream_for "#{e}_#{et}"
          end
        end
      else
        stream_from 'api_feedback'
        stream_from 'api_flag_logs'
        stream_from 'api_deletion_logs'
        stream_from 'api_statistics'
      end
    else
      reject
    end
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end


text:
# frozen_string_literal: true

# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
class PostsChannel < ApplicationCable::Channel
  def subscribed
    if params[:post_id].present?
      stream_from "posts_#{params[:post_id]}"
    else
      stream_from 'posts_realtime'
    end
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end


text:
# frozen_string_literal: true

# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
class PingsChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'smokey_pings'
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end


text:
# frozen_string_literal: true

# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
class SmokeDetectorChannel < ApplicationCable::Channel
  def subscribed
    if SmokeDetector.find_by(access_token: params[:key]).present?
      stream_from 'smokedetector_messages'
    else
      reject
    end
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end


text:
# frozen_string_literal: true

# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
class GithubChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'github_new_commit'
  end
end


text:
# frozen_string_literal: true

# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
class TopbarChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'topbar'
    ActionCable.server.broadcast 'topbar', commit: CurrentCommit
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end


text:
# frozen_string_literal: true

class FlagLogsChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'flag_logs'
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end


text:
# frozen_string_literal: true

class RedisLogChannel < ApplicationCable::Channel
  def subscribed
    if !current_user.nil? && current_user.has_role?(:developer)
      %i[status path session].each do |filter|
        if params[filter].present?
          stream_from "redis_log_#{filter}_#{params[filter]}"
          return # rubocop:disable Lint/NonLocalExitFromIterator
        end
      end
      stream_from 'redis_log_channel'
    else
      reject
    end
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def new; end
end


text:
# frozen_string_literal: true

# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_current_user
    end

    private

    def find_current_user
      cookies.signed['user.expires_at'] && cookies.signed['user.expires_at'] > Time.now && User.find_by(id: cookies.signed['user.id'])
    end
  end
end


text:
# frozen_string_literal: true

# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end


text:
# frozen_string_literal: true

class AnnouncementsMailer < ApplicationMailer
  default from: 'MS Announcements <metasmoke@erwaysoftware.com>'

  def announce(announcement)
    @announcement = announcement
    users = User.where(announcement_emails: true).where.not('email IS NULL OR email = ""')
    emails = users.map(&:email)

    emails.each do |e|
      mail(to: e, subject: 'New metasmoke announcement')
    end
  end
end


text:
# frozen_string_literal: true

class DomainGroupsMailer < ApplicationMailer
  default from: 'Charcoal SpamWatch Alerts <alerts@charcoal-se.org>'

  def domain_group_summary(preference)
    @type = preference.type
    @addressee = preference.addressee
    @group = preference.reference

    @last_email_date = (Date.today - preference.frequency.days).to_time.iso8601
    @domains = DomainGroup.where(id: @group.id).joins(Arel.sql('INNER JOIN domain_groups_spam_domains j ON j.domain_group_id = domain_groups.id'))
                          .where('j.created_at >= ?', @last_email_date)

    @preference.update(next: (Date.today + preference.frequency.days).to_time.iso8601)

    mail(to: @addressee.email_address, subject: "Your SpamWatch summary for #{@group.name}")
  end
end


text:
# frozen_string_literal: true

class HairOnFireMailer < ApplicationMailer
  default from: 'Hair on Fire <metasmoke@erwaysoftware.com>'

  def smokey_down_email(smoke_detector)
    @last_ping_date = smoke_detector.last_ping
    @location = smoke_detector.location

    mail(
      to: smoke_detector.user.try(:email) || 'undo@erwaysoftware.com',
      cc: 'teward@ubuntu.com; undo@erwaysoftware.com; hello@artofcode.co.uk',
      subject: "[SmokeDetector] status DOWN (last seen #{@last_ping_date})"
    )
  end
end


text:
# frozen_string_literal: true

class ApplicationMailer < ActionMailer::Base
  default from: 'metasmoke@erwaysoftware.com'
  layout 'mailer'
end


text:
# frozen_string_literal: true

class PostsController < ApplicationController
  protect_from_forgery except: [:create]
  before_action :check_if_smokedetector, only: :create
  before_action :set_post, only: %i[remove_domain add_domain needs_admin feedbacksapi reindex_feedback cast_spam_flag delete_post]
  before_action :authenticate_user!, only: %i[reindex_feedback cast_spam_flag]
  before_action :verify_developer, only: %i[reindex_feedback delete_post]
  before_action :verify_reviewer, only: %i[feedback cast_spam_flag]
  before_action :verify_core, only: %i[remove_domain add_domain]
  before_action :check_if_duplicate, only: :create

  def show
    begin
      @post = Post.joins('LEFT JOIN `sites` ON `sites`.`id` = `posts`.`site_id`')
                  .joins(:reasons)
                  .includes(:feedbacks)
                  .select(Arel.sql('posts.*, sites.site_logo, SUM(reasons.weight) AS reason_weight'))
                  .find(params[:id])
    rescue # rubocop:disable Lint/HandleExceptions
    end
    @post = Post.find params[:id] if @post&.id.nil?

    @is_review_item = false

    not_found if @post&.id.nil?
  end

  def add_domain
    domain = SpamDomain.find_by(domain: params[:domain_name])
    if !domain.present?
      flash[:warning] = "Domain #{params[:domain_name]} not found"
    else
      PostSpamDomain.create(post: @post, spam_domain: domain, added_by: current_user)
    end
    redirect_back(fallback_location: post_path(@post))
  end

  def remove_domain
    domain = PostSpamDomain.find_by(post: @post, spam_domain: params[:domain_id])
    if domain.nil? || domain.added_by.nil?
      flash[:warning] = 'You can only delete user added spam domains'
    else
      domain.custom_delete
    end
    redirect_back(fallback_location: post_path(@post))
  end

  # Render bodies on-demand for fancy expanding rows
  def body
    @post = Post.where(id: params[:id]).select(:body, :markdown, :id).includes(:reasons).first
    render layout: false
  end

  def latest
    redirect_to url_for(controller: :posts, action: :show, id: Post.select(Arel.sql('id')).last.id.to_s)
  end

  def by_url
    @posts = Post.where(link: params[:url])
    count = @posts.count

    if count < 1
      flash[:danger] = "Post not found for #{params[:url]}. It may have been reported during a period of metasmoke downtime."
      redirect_to posts_path
    elsif count == 1
      redirect_to url_for(controller: :posts, action: :show, id: @posts.first.id)
    else
      flash.now[:info] = 'Multiple records were found for this URL; pick the one you meant from this list.'
    end
  end

  def by_uid
    @posts = Post.joins(:site).where(sites: { api_parameter: params[:api_param] }, posts: { native_id: params[:native_id] })
    count = @posts.count

    if count < 1
      flash[:danger] = "Post not found for #{params[:api_param]}/#{params[:native_id]}. "\
                       'It may have been reported during a period of metasmoke downtime.'
      redirect_to posts_path
    elsif count == 1
      redirect_to url_for(controller: :posts, action: :show, id: @posts.first.id)
    else
      flash.now[:info] = 'Multiple records were found for this URL; pick the one you meant from this list.'
      render action: :by_url
    end
  end

  def recentpostsapi
    posts = Rails.cache.fetch('last-posts', expires_in: 30.seconds) do
      Post.joins(:site).select(Arel.sql('posts.id, posts.title, posts.link, posts.created_at, sites.site_logo')).order(:created_at).last(100)
    end

    posts.each do |p|
      p.title.gsub! '<', '<'
      p.title.gsub! '>', '>'
    end

    posts = posts.last([params[:size].to_i, 100].min).reverse

    render json: posts, status: 200
  end

  def feedbacksapi
    if user_signed_in?
      render json: @post.feedbacks.select(:id, :chat_user_id, :user_id, feedback_type)
    else
      render json: { error: 'You must be signed in to use the feedbacks API.' }
    end
  end

  # GET /posts
  # GET /posts.json
  def index
    page_num = [params[:page].to_i - 1, 0].max
    per_page = 100
    page = [page_num * per_page, (page_num + 1) * per_page - 1]
    k = 'posts'
    if params[:filter] == 'undeleted'
      k = 'undeleted_posts'
      # Move this code into Redis::Post#update_undeleted_cache
      Redis::Post.all(type: :set).difference('deleted').store('undeleted_posts/pre')
      Redis::Post.all(type: :zset).intersect('undeleted_posts/pre').store('undeleted_posts')
    end
    start = Time.now
    # Use paginate
    @posts = redis.zrevrange(k, *page).map do |id|
      Redis::Post.new(id)
    end
    endt = Time.now
    Rails.logger.info "Took #{endt - start} to build posts"
    @posts.define_singleton_method(:total_pages) { Redis::Post.all(type: :zset).cardinality / 100 }
    @posts.define_singleton_method(:current_page) { page_num + 1 }
    @sites = Site.where(id: @posts.map(&:site_id)).to_a
  end

  # POST /posts
  # POST /posts.json
  def create
    @post = Post.new(post_params)

    @post.smoke_detector = @smoke_detector
    @post.site = Site.find_by(site_domain: URI.parse(@post.link).host)

    params['post']['reasons'].each do |r|
      reason = Reason.find_or_create_by(reason_name: r.split('(').first.strip.humanize)

      reason.last_post_title = @post.title
      reason.inactive = false
      reason.save!

      @post.reasons << reason
    end

    begin
      user_id = @post.user_link.scan(%r{/u(sers)?/(\d*)}).first.second

      hash = { site_id: @post.site_id, user_id: user_id }
      se_user = StackExchangeUser.find_or_create_by(hash)
      se_user.reputation = @post.user_reputation
      se_user.username = @post.username

      se_user.save!

      @post.stack_exchange_user = se_user
    rescue # rubocop:disable Lint/HandleExceptions
    end

    less_important_things(@post)

    respond_to do |format|
      if @post.save
        # Start autoflagging
        Rails.logger.warn "[autoflagging] #{@post.id}: post.save succeeded"

        AutoflagJob.perform_later @post.id

        format.json { render status: :created, plain: 'OK' }
      else
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  def needs_admin
    flag = Flag.new
    flag.reason = params[:flag][:reason]
    flag.user_id = current_user.id unless current_user.nil?
    flag.post = @post
    flag.is_completed = false

    if flag.save
      render plain: 'OK'
    else
      render plain: 'Save failed.', status: :internal_server_error
    end
  end

  def reindex_feedback
    if @post.update_feedback_cache
      flash[:success] = 'Feedback reindexed and corrected.'
    else
      flash[:info] = 'Feedback reindexed; no change.'
    end
    redirect_to url_for(controller: :posts, action: :show, id: @post.id)
  end

  def delete_post
    if @post.destroy
      flash[:success] = 'Post destroyed successfully'
    else
      flash[:warning] = 'Something went wrong when destroying post.'
    end

    SmokeDetector.send_message_to_charcoal "Metasmoke post #{@post.id} (#{@post.title}) destroyed by #{@current_user.username}"

    redirect_to posts_path
  end

  def cast_spam_flag
    flag_type = params[:flag_type] || 'spam'
    feedback = Feedback.new(feedback_type: 'tpu-',
                            user_id: current_user.id,
                            post_id: @post.id)

    flash[:danger] = 'Unable to save feedback. Ping Undo.' unless feedback.save

    unless current_user.write_authenticated && current_user.flags_enabled
      flash[:warning] = 'You must be write-authenticated to cast a spam flag.'
      redirect_to(authentication_status_path) && return
    end

    if flag_type == 'spam'
      result, message = current_user.spam_flag(@post, false)
    elsif flag_type == 'abusive'
      result, message = current_user.abusive_flag(@post, false)
    else
      not_found
    end

    FlagLog.create(success: result, error_message: result.present? ? nil : message,
                   is_dry_run: false, flag_condition: nil,
                   user: current_user, post: @post, backoff: result.present? ? message : 0,
                   site_id: @post.site_id, is_auto: false)

    if result
      flash[:success] = "#{flag_type.capitalize} flag cast successfully."

    else
      flash[:danger] = "#{flag_type.capitalize} flag not cast: #{message}"
    end
    redirect_to url_for(controller: :posts, action: :show, id: params[:id])
  end

  def feedback
    not_found unless %w[tp fp naa].include? params[:feedback_type]
    @post = Post.find params[:post_id]
    @post.feedbacks.create user: current_user, feedback_type: params[:feedback_type]
  end

  private

  # Use callbacks to share common setup or constraints between actions.
  def set_post
    @post = Post.find(params[:id])
  end

  # Never trust parameters from the scary internet, only allow the white list through.
  def post_params
    permitted = %w[title body markdown link post_creation_date reasons username user_link why user_reputation score upvote_count downvote_count]
    params.require(:post).permit(permitted)
  end

  def less_important_things(post)
    ri = ReviewItem.new(reviewable: post, queue: ReviewQueue['posts'], completed: false)
    ri_success = ri.save
    return if ri_success
    Rails.logger.warn "[post-create] review item create failed: #{ri.errors.full_messages.join(', ')}"
  end

  DUPLICATE_CHECK_FIELDS = %i[title body username why].freeze

  def check_if_duplicate
    old_post = Post.where(link: post_params[:link]).select(*DUPLICATE_CHECK_FIELDS).order(id: :desc).limit(1).first
    render status: 200, plain: 'Duplicate Report' if !old_post.nil? && DUPLICATE_CHECK_FIELDS.all? { |attr| old_post.send(attr) == post_params[attr] }
  end
end


text:
# frozen_string_literal: true

class PostCommentsController < ApplicationController
  before_action :verify_reviewer
  before_action :set_comment, except: [:create]
  before_action :verify_access, except: [:create]
  skip_before_action :verify_authenticity_token, only: [:update]

  def create
    @comment = PostComment.new comment_params.merge(user: current_user)
    flash[:danger] = 'Failed to post your comment.' unless @comment.save
    redirect_back fallback_location: post_path(@comment.post_id)
  end

  def update
    unless @comment.update(text: params[:text])
      flash[:danger] = 'Failed to update your comment.'
    end
    redirect_back fallback_location: post_path(@comment.post_id)
  end

  def destroy
    post_id = @comment.post_id
    if @comment.destroy
      flash[:success] = 'Comment removed.'
    else
      flash[:danger] = 'Failed to remove your comment.'
    end
    redirect_to post_path(post_id)
  end

  def text
    render json: { text: @comment.text }
  end

  private

  def comment_params
    params.require(:post_comment).permit(:post_id, :text)
  end

  def set_comment
    @comment = PostComment.find params[:id]
  end

  def verify_access
    return if current_user == @comment.user || current_user.has_role?(:admin)
    not_found
  end
end


text:
# frozen_string_literal: true

class APIController < ApplicationController
  before_action :verify_key, except: %i[filter_generator api_docs filter_fields calculate_filter]
  before_action :verify_trusted_key, only: [:regex_search]
  before_action :set_pagesize, except: %i[filter_generator api_docs]
  before_action :verify_write_token, only: %i[create_feedback report_post spam_flag add_domain_tag]
  skip_before_action :verify_authenticity_token, only: %i[posts_by_url create_feedback report_post spam_flag post_deleted add_domain_tag
                                                          calculate_filter]

  # Public routes

  def api_docs
    redirect_to 'https://charcoal-se.org/ms/API-Documentation-(V2)'
  end

  # Routes for developer use

  def filter_generator; end

  # Read routes: Posts

  def posts
    filter = 'AAAAAAAAAAPHx4AAAAAAAUA='
    @posts = Post.where(id: params[:ids].split(';'))
                 .select(select_fields(filter))
                 .order(id: :desc)
                 .left_joins(:feedbacks)
                 .left_joins(:deletion_logs)
                 .includes(flag_logs: [:user])
    @results = @posts.paginate(page: params[:page], per_page: @pagesize)
    @more = has_more?(params[:page], @results.count)
    @results = @results.group(:id)
    render formats: :json
  end

  def posts_by_feedback
    filter = 'AAAAAAAAAAPHx4AAAAAAAUA='
    @posts = Post.all.joins(:feedbacks)
                 .where(feedbacks: { feedback_type: params[:type] })
                 .select(select_fields(filter))
                 .order(id: :desc)
                 .includes(:feedbacks)
                 .includes(flag_logs: [:user])
    results = @posts.paginate(page: params[:page], per_page: @pagesize)
    render json: { items: results, has_more: has_more?(params[:page], results.count) }
  end

  def posts_by_url
    filter = 'AAAAAAAAAAPHx4AAAAAAAUA='
    @posts = Post.where(link: params[:urls].split(';'))
                 .select(select_fields(filter))
                 .order(id: :desc)
                 .includes(feedbacks: [:user])
                 .includes(:deletion_logs)
                 .includes(flag_logs: [:user])
    @results = @posts.paginate(page: params[:page], per_page: @pagesize)
    @more = has_more?(params[:page], @results.count)
    @results = @results.group(:id)
    render 'posts.json.jbuilder'
  end

  def posts_by_site
    filter = 'AAAAAAAAAAPHx4AAAAAAAUA='
    @posts = Post.joins(:site)
                 .where(sites: { site_domain: params[:site] })
                 .select(select_fields(filter))
                 .order(id: :desc)
                 .includes(:feedbacks)
                 .includes(flag_logs: [:user])
    results = @posts.paginate(page: params[:page], per_page: @pagesize)
    render json: { items: results, has_more: has_more?(params[:page], results.count) }
  end

  def posts_by_daterange
    filter = 'AAAAAAAAAAPHx4AAAAAAAUA='
    @posts = Post.where(created_at: DateTime.strptime(params[:from_date], '%s')..DateTime.strptime(params[:to_date], '%s'))
                 .includes(:feedbacks)
                 .includes(flag_logs: [:user])
    results = @posts.select(select_fields(filter)).order(id: :desc).paginate(page: params[:page], per_page: @pagesize)
    render json: { items: results, has_more: has_more?(params[:page], results.count) }
  end

  def undeleted_posts
    filter = 'AAAAAAAAAAPHx4AAAAAAAUA='
    @posts = Post.where(deleted_at: nil).select(select_fields(filter)).includes(:feedbacks).includes(flag_logs: [:user])
    results = @posts.order(id: :desc).paginate(page: params[:page], per_page: @pagesize)
    render json: { items: results, has_more: has_more?(params[:page], results.count) }
  end

  def post_feedback
    filter = 'AAAAAI0JAAAAAAAAAAAAAAA='
    @feedbacks = Feedback.where(post_id: params[:id]).select(select_fields(filter)).order(id: :desc)
    results = @feedbacks.paginate(page: params[:page], per_page: @pagesize)
    render json: { items: results, has_more: has_more?(params[:page], results.count) }
  end

  def post_reasons
    filter = 'AAAAAAAAAAAAABgAAAAAAAA='
    @reasons = Reason.joins(:posts).where(posts_reasons: { post_id: params[:id] }).select(select_fields(filter)).order(id: :desc)
    results = @reasons.paginate(page: params[:page], per_page: @pagesize)
    render json: { items: results, has_more: has_more?(params[:page], results.count) }
  end

  def post_valid_feedback
    @post = Post.find params[:id]
    render formats: :json
  end

  def search_posts
    filter = 'AAAAAAAAAAPHx4AAAAAAAUA='
    @posts = Post.all
    if params[:feedback_type].present?
      @posts = @posts.includes(:feedbacks).where(feedbacks: { feedback_type: params[:feedback_type] })
    end
    if params[:site].present?
      @posts = @posts.joins(:site).where(sites: { site_domain: params[:site] })
    end
    if params[:from_date].present?
      @posts = @posts.where('`posts`.`created_at` > ?', DateTime.strptime(params[:from_date], '%s'))
    end
    if params[:to_date].present?
      @posts = @posts.where('`posts`.`created_at` < ?', DateTime.strptime(params[:to_date], '%s'))
    end
    if params[:autoflagged].present?
      @posts = @posts.where(autoflagged: params[:autoflagged])
    end
    results = @posts.select(select_fields(filter)).order(id: :desc).paginate(page: params[:page], per_page: @pagesize)
    render json: { items: results, has_more: has_more?(params[:page], results.count) }
  end

  # Read routes: Reasons

  def reasons
    filter = 'AAAAAAAAAAAAABgAAAAAAAA='
    @reasons = Reason.where(id: params[:ids].split(';')).select(select_fields(filter)).order(id: :desc)
    results = @reasons.paginate(page: params[:page], per_page: @pagesize)
    render json: { items: results, has_more: has_more?(params[:page], results.count) }
  end

  def all_reasons
    filter = 'AAAAAAAAAAAAABgAAAAAAAA='
    @reasons = Reason.all.select(select_fields(filter)).order(id: :desc)
    results = @reasons.paginate(page: params[:page], per_page: @pagesize)
    render json: { items: results, has_more: has_more?(params[:page], results.count) }
  end

  def reason_posts
    filter = 'AAAAAAAAAAPHx4AAAAAAAUA='
    @posts = Post.joins(:posts_reasons).where(posts_reasons: { reason_id: params[:id] }).select(select_fields(filter)).order(id: :desc)
    results = @posts.paginate(page: params[:page], per_page: @pagesize)
    render json: { items: results, has_more: has_more?(params[:page], results.count) }
  end

  # Read routes: BlacklistedWebsites

  def blacklisted_websites
    @websites = BlacklistedWebsite.active
    results = @websites.order(id: :desc).paginate(page: params[:page], per_page: @pagesize)
    render json: { items: results, has_more: has_more?(params[:page], results.count) }
  end

  # Read routes: Users

  def users_with_code_privs
    chat_ids = User.blacklist_managers.pluck(:stackexchange_chat_id, :stackoverflow_chat_id, :meta_stackexchange_chat_id)

    items = {}
    %w[stackexchange_chat_ids stackoverflow_chat_ids meta_stackexchange_chat_ids].each_with_index do |name, index|
      items[name] = chat_ids.map { |a| a[index] }.select(&:present?)
    end

    render json: { items: items }
  end

  def users
    filter = 'AAAAAAAAAAAAAAAAAAAB+AA='

    users = if params[:role]
              User.with_role(params[:role])
            else
              User.all
            end

    @users = users.select(select_fields(filter)).order(id: :asc).paginate(page: params[:page], per_page: @pagesize)
    @has_more = has_more?(params[:page], @users.count)
    render formats: [:json]
  end

  # Read routes Status

  def current_status
    render json: { last_ping: SmokeDetector.where.not(location: params[:except]).maximum(:last_ping) }
  end

  # Read routes: SmokeDetectors

  def smoke_detectors
    filter = 'AAAAAAAAAAAAAAAADQAAAAE='
    fields = select_fields(filter) - ['smoke_detectors.access_token']

    smokeys = SmokeDetector.all.select(fields).order(id: :asc)
    smokeys = smokeys.where(user_id: params[:owner]) if params[:owner].present?

    render json: { items: smokeys }
  end

  # Read routes: App stuff

  def filter_fields
    i = -1
    render json: AppConfig['api_field_mappings'].map { |f| [f, i += 1] }.to_h
  end

  def spam_last_week
    render json: Site.joins(:posts).where(posts: { is_tp: true, created_at: 1.week.ago.to_date..Date.today })
      .group(Arel.sql('sites.site_name')).count
  end

  def detailed_ttd
    no_flags = Post.group_by_hour_of_day('`posts`.`created_at`')
                   .select(Arel.sql('AVG(TIMESTAMPDIFF(SECOND, `posts`.`created_at`, `deletion_logs`.`created_at`)) as time_to_deletion'))
                   .joins(:deletion_logs)
                   .where(is_tp: true)
                   .where('`posts`.`created_at` < ?', Date.new(2017, 1, 1))
                   .where('TIMESTAMPDIFF(SECOND, `posts`.`created_at`, `deletion_logs`.`created_at`) <= 3600').relation.each_with_index
                   .map { |a, i| [i, a.time_to_deletion.round(0)] }
    one_flag = Post.group_by_hour_of_day('`posts`.`created_at`')
                   .select(Arel.sql('AVG(TIMESTAMPDIFF(SECOND, `posts`.`created_at`, `deletion_logs`.`created_at`)) as time_to_deletion'))
                   .joins(:deletion_logs)
                   .where(is_tp: true)
                   .where('`posts`.`created_at` >= ?', Date.new(2017, 1, 1))
                   .where('`posts`.`created_at` < ?', Date.new(2017, 2, 14))
                   .where('TIMESTAMPDIFF(SECOND, `posts`.`created_at`, `deletion_logs`.`created_at`) <= 3600').relation.each_with_index
                   .map { |a, i| [i, a.time_to_deletion.round(0)] }
    three_flags = Post.group_by_hour_of_day('`posts`.`created_at`')
                      .select(Arel.sql('AVG(TIMESTAMPDIFF(SECOND, `posts`.`created_at`, `deletion_logs`.`created_at`)) as time_to_deletion'))
                      .joins(:deletion_logs)
                      .where(is_tp: true)
                      .where('`posts`.`created_at` >= ?', Date.new(2017, 2, 14))
                      .where('TIMESTAMPDIFF(SECOND, `posts`.`created_at`, `deletion_logs`.`created_at`) <= 3600').relation.each_with_index
                      .map { |a, i| [i, a.time_to_deletion.round(0)] }
    render json: [
      { name: '0 flags', data: no_flags },
      { name: '1 flag', data: one_flag },
      { name: '3 flags', data: three_flags }
    ]
  end

  # Write routes

  def create_feedback
    @post = Post.find params[:id]
    @feedback = Feedback.new(user: @user, post: @post, api_key: @key)
    @feedback.feedback_type = params[:type]

    if @post.question? && @feedback.is_naa?
      render status: 500, json: { error_name: 'failed', error_code: 500, error_message: "NAA feedback isn't allowed on questions" }
      return
    end

    if @feedback.save
      if @feedback.is_naa?
        begin
          ActionCable.server.broadcast 'smokedetector_messages', naa: { post_link: @post.link }
        rescue # rubocop:disable Lint/HandleExceptions
        end
      elsif @feedback.is_negative?
        begin
          ActionCable.server.broadcast 'smokedetector_messages', fp: { post_link: @post.link }
        rescue # rubocop:disable Lint/HandleExceptions
        end
      end
      render json: @post.feedbacks, status: 201
    else
      render status: 500, json: { error_name: 'failed', error_code: 500, error_message: 'Feedback object failed to save.' }
    end
  end

  def report_post
    # We don't create any posts here, just send them on to Smokey to do all the processing
    ActionCable.server.broadcast 'smokedetector_messages', report: { user: @user.username, post_link: params[:post_link] }

    render plain: 'OK', status: 201
  end

  def spam_flag
    @post = Post.find params[:id]

    unless @user.write_authenticated && @user.flags_enabled
      render status: 409, json: {
        error_name: 'not_write_authenticated',
        error_code: 409,
        error_message: 'Current user is not write-authenticated.'
      }
      return
    end

    status, message = @user.spam_flag(@post, false)
    FlagLog.create(
      success: status,
      error_message: status.present? ? nil : message,
      is_dry_run: false,
      flag_condition: nil,
      user: @user,
      post: @post,
      backoff: status.present? ? message : 0,
      site_id: @post.site_id,
      is_auto: false,
      api_key: @key
    )
    if status
      render json: { status: 'success', backoff: message }
    else
      render status: 500, json: { status: 'failed', message: message }
    end
  end

  def post_deleted
    unless @key.is_trusted
      render(status: 403, json: { status: 'failed', message: 'The API key used to make the request is not trusted.' }) && return
    end

    post = Post.find params[:id]
    dl = post.deletion_logs.new(api_key_id: @key.id, is_deleted: true)

    if dl.save
      render json: { status: 'success' }
    else
      render status: 500, json: { status: 'failed', message: 'The deletion log failed to save.' }
    end
  end

  def post_domains
    post = Post.find params[:id]
    results = post.spam_domains
    render json: { items: results.paginate(page: params[:page], per_page: @pagesize), has_more: has_more?(params[:page], results.count) }
  end

  def domain_tags
    domain = SpamDomain.find params[:id]
    results = domain.domain_tags
    render json: { items: results.paginate(page: params[:page], per_page: @pagesize), has_more: has_more?(params[:page], results.count) }
  end

  def add_domain_tag
    domain = SpamDomain.find params[:id]
    tag = DomainTag.find_or_create_by name: params[:tag]
    domain.domain_tags << tag
    results = domain.domain_tags.limit(@pagesize)
    render json: { success: true, items: results, more_url: api_domain_tags_url(domain) }
  end

  def calculate_filter
    filter = Filterator::V2.filter_from_fields(params[:fields])
    render json: { filter: filter }
  end

  private

  def verify_key
    @key = APIKey.find_by(key: params[:key])
    return if params[:key].present? && @key.present?
    smokey = SmokeDetector.find_by(access_token: params[:key])
    return if smokey.present?
    render status: 403, json: {
      error_name: 'unauthenticated',
      error_code: 403,
      error_message: 'No key was passed or the passed key is invalid.'
    }
  end

  def set_pagesize
    @pagesize = [(params[:per_page] || 10).to_i, 100].min
  end

  def has_more?(page, result_count) # rubocop:disable Style/PredicateName
    (page || 1).to_i * @pagesize < result_count
  end

  def verify_write_token
    # This method deliberately doesn't check expiry: tokens are valid for authorization
    # forever, but can only be fetched using the code in the first 10 minutes.
    @token = APIToken.where(token: params[:token], api_key: @key)
    unless @token.any?
      render status: 401, json: {
        error_name: 'unauthorized',
        error_code: 401,
        error_message: 'The token provided does not supply authorization to perform this action.'
      }
      return
    end

    @token = @token.first
    @user = @token.user
  end

  def select_fields(default = '')
    Filterator::V1.fields_from_string(params[:filter] || default)
  end

  def verify_trusted_key
    return if @key.present? && @key.is_trusted
    render status: 403, json: {
      error_name: 'trusted_key_required',
      error_code: 403,
      error_message: 'The requested action requires the presented key to be trusted.'
    }
  end
end


text:
# frozen_string_literal: true

class AbuseCommentsController < ApplicationController
  before_action :verify_core
  before_action :set_comment, except: [:create]
  before_action :verify_permission, only: %w[update destroy]
  skip_before_action :verify_authenticity_token, only: [:update]

  def create
    @comment = AbuseComment.new comment_params.merge(user: current_user)
    @comment.text = @comment.text.gsub(/#(\d+)/, '[#\1](https://metasmoke.erwaysoftware.com/abuse/reports/\1)')
    flash[:danger] = 'Failed to post your comment.' unless @comment.save
    redirect_to abuse_report_path(@comment.abuse_report_id)
  end

  def update
    unless @comment.update(text: params[:text])
      flash[:danger] = 'Failed to update your comment.'
    end
    redirect_to abuse_report_path(@comment.abuse_report_id)
  end

  def destroy
    report_id = @comment.abuse_report_id
    if @comment.destroy
      flash[:success] = 'Comment removed.'
    else
      flash[:danger] = 'Failed to remove your comment.'
    end
    redirect_to abuse_report_path(report_id)
  end

  def text
    render json: { text: @comment.text }
  end

  private

  def comment_params
    params.require(:abuse_comment).permit(:abuse_report_id, :text)
  end

  def set_comment
    @comment = AbuseComment.find params[:id]
  end

  def verify_permission
    return if @comment.user_id == current_user.id || current_user.has_role?(:admin)
    not_found
  end
end


text:
# frozen_string_literal: true

class ReviewQueuesController < ApplicationController
  before_action :set_queue, except: [:index]
  before_action :verify_permissions, except: [:index]
  before_action :verify_developer, only: [:recheck_items]
  before_action :verify_admin, only: [:delete]

  def index
    @queues = ReviewQueue.all.includes(:items)
  end

  def queue; end

  def next_item
    response.cache_control = 'max-age=0, private, must-revalidate, no-store'
    reviewable_table = @queue.reviewable_type&.pluralize&.underscore
    unreviewed = if params[:site_id].present? && !reviewable_table.nil?
                   @queue.next_items(current_user) do |c|
                     c.joins("INNER JOIN `#{reviewable_table}` AS reviewable ON reviewable.id = review_items.reviewable_id")
                      .where(reviewable: filter_params([:site_id]))
                   end
                 else
                   @queue.next_items(current_user)
                 end

    unreviewed_count = unreviewed.count

    while !unreviewed.empty? && unreviewed.first.reviewable.nil?
      unreviewed.first.update(completed: true)
      unreviewed = ReviewItem.unreviewed_by(@queue, current_user)
    end

    if unreviewed.empty?
      render plain: "You've reviewed all available items!"
    else
      item = unreviewed.first
      render "#{item.reviewable_type.underscore.pluralize}/_review_item.html.erb",
             locals: { queue: @queue, item: item, per_user_unreviewed_count: unreviewed_count }, layout: nil
    end
  end

  def submit
    unless (@queue.responses.map { |r| r[1] } + ['skip']).include? params[:response]
      render json: { status: 'invalid' }, status: 400
      return
    end

    @item = ReviewItem.find params[:item_id]

    # Prevent the same item from being reviewed after it is completed, or twice by the same user.
    if (@item.completed && ReviewResult.where(item: @item).where.not(result: 'skip').exists?) ||
       ReviewResult.where(user: current_user, item: @item).where.not(result: 'skip').exists?

      render json: { status: 'duplicate' }, status: 409
      return
    end

    ReviewResult.create user: current_user, result: params[:response], item: @item

    unless params[:response] == 'skip'
      @item.reviewable.custom_review_action(@queue, @item, current_user, params[:response]) if @item.reviewable.respond_to? :custom_review_action
      if @item.reviewable.respond_to?(:should_dq?) && @item.reviewable.should_dq?(@queue)
        @item.update(completed: true)
      end
    end

    render json: { status: 'ok' }
  end

  def item
    @item = ReviewItem.find(params[:item_id])
    render :queue
  end

  def reviews
    @all = params[:all].present? && params[:all] == '1'
    @reviews = ReviewResult.joins(:item).includes(:item).where(review_items: { review_queue_id: @queue })
    @reviews = @reviews.where(Arel.sql('review_items.reviewable_id IS NOT NULL AND review_items.reviewable_type IS NOT NULL'))
    @reviews = @reviews.where(user: current_user) if @all == false
    @reviews = @reviews.where(user_id: params[:user]) if params[:user].present?
    @reviews = @reviews.where(result: params[:response]) if params[:response].present?
    @reviews = @reviews.joins(:user, user: :roles).where(roles: { name: params[:role] }) if params[:role].present?
    @reviews = @reviews.order(created_at: :desc).paginate(page: params[:page], per_page: 100)
  end

  def recheck_items
    RecheckReviewQueueJob.perform_later(@queue)
    flash[:info] = 'Checking started in background.'
    redirect_back fallback_location: review_queues_path
  end

  def delete
    @review = ReviewResult.find(params[:id])
    @review.destroy
    head :no_content
  end

  private

  def set_queue
    @queue = ReviewQueue[params[:name]]
    not_found if @queue.nil?
  end

  def verify_permissions
    return if user_signed_in? && current_user.has_role?(@queue.privileges)
    not_found
  end

  def filter_params(allowed)
    allowed.map { |p| params[p].present? ? [p, params[p]] : nil }.compact.to_h
  end
end


text:
# frozen_string_literal: true

class MicroAuthController < ApplicationController
  before_action :authenticate_user!, except: [:token]
  before_action :verify_key, except: %i[invalid_key authorized]
  before_action :set_token, only: [:authorized]

  def token_request; end

  def authorize
    unless current_user.has_role?(:reviewer)
      flash[:danger] = 'Your account is not approved to use the write API.'
      redirect_to(url_for(controller: :micro_auth, action: :token_request)) && return
    end

    @token = APIToken.new(user: current_user, api_key: @api_key, code: generate_code(7), token: generate_code(64), expiry: 10.minutes.from_now)
    if @token.save
      if params[:redirect].present? && !params[:redirect].empty?
        redirect_to "#{params[:redirect]}?token=#{@token.token}"
      else
        redirect_to url_for(controller: :micro_auth, action: :authorized, code: @token.code, token_id: @token.id)
      end
    else
      flash[:danger] = "Can't create a write token right now - ask an admin to look at the server logs."
      redirect_to url_for(controller: :micro_auth, action: :token_request)
    end
  end

  def authorized
    @token = APIToken.find params[:token_id]
    return if current_user.has_role?(:developer) || current_user == @token.user
    redirect_to 'https://www.youtube.com/watch?v=oHg5SJYRHA0'
  end

  def reject; end

  def token
    code = params[:code]
    token = APIToken.where(code: code, api_key: @api_key)
    user = token.first&.user
    if token.any? && !token.first.expiry.past?
      render json: { token: token.first.token, user: { id: user.id, username: user.username, se_account_id: user.stack_exchange_account_id } }
    else
      render json: {
        error_name: 'token not found',
        error_code: 404,
        error_message: 'There was no token found matching the key and code.'
      }, status: 404
    end
  end

  def invalid_key; end

  private

  def generate_code(len)
    SecureRandom.hex((len / 2.0).ceil)
  end

  def verify_key
    @api_key = APIKey.find_by(key: params[:key])
    return if params[:key].present? && @api_key.present?
    render :invalid_key, status: 400
  end

  def set_token
    @token = APIToken.find params[:token_id]
  end
end


text:
# frozen_string_literal: true

class CustomSessionsController < Devise::SessionsController
  protect_from_forgery except: [:create]

  @@first_factor = [] # rubocop:disable Style/ClassVars

  def new
    super
  end

  def create
    super do |user|
      if user.present? && user.enabled_2fa
        id = user.id
        @@first_factor << id
        sign_out user
        redirect_to(url_for(controller: :custom_sessions, action: :verify_2fa, uid: id)) && return
      end
    end
  end

  def destroy
    super
  end

  def verify_2fa; end

  def verify_code
    target_user = User.find params[:uid]

    if target_user.two_factor_token.blank?
      flash[:danger] = 'I have no idea how you got here, but something is very wrong.'
      redirect_to(root_path) && return
    end

    totp = ROTP::TOTP.new(target_user.two_factor_token)
    if totp.verify(params[:code], drift_ahead: 30, drift_behind: 30)
      if @@first_factor.include? params[:uid].to_i
        @@first_factor.delete params[:uid].to_i
        sign_in_and_redirect User.find(params[:uid])
      else
        flash[:danger] = "You haven't completed password authentication yet!"
        redirect_to new_session_path(target_user)
      end
    else
      flash[:danger] = "That's not the right code."
      redirect_to url_for(controller: :custom_sessions, action: :verify_2fa, uid: params[:uid])
    end
  end
end


text:
# frozen_string_literal: true

class SpamWavesController < ApplicationController
  before_action :verify_admin
  before_action :set_wave, except: %i[index new create preview]

  def index
    @active = SpamWave.active.order(created_at: :desc).paginate(page: params[:page], per_page: 30)
    @expired = SpamWave.expired.order(created_at: :desc).paginate(page: params[:page], per_page: 30)
  end

  def show; end

  def new
    @errors = []
  end

  def create
    sites = params[:sites].present? && params[:sites].respond_to?(:map) ? params[:sites].map { |s| Site.find(s) } : []
    @wave = SpamWave.new(name: params[:name], user: current_user, expiry: params[:expiry], max_flags: params[:max_flags],
                         conditions: params[:conditions], sites: sites, created_at: DateTime.now)
    if @wave.save
      redirect_to spam_wave_path(@wave)
    else
      @errors = @wave.errors
      render :new
    end
  end

  def preview
    sites = params[:sites].present? && params[:sites].respond_to?(:map) ? params[:sites].map { |s| Site.find(s) } : []
    @wave = SpamWave.new(name: params[:name], user: current_user, expiry: params[:expiry], max_flags: params[:max_flags],
                         conditions: params[:conditions], sites: sites, created_at: DateTime.now)
    posts = @wave.posts
    @posts = Post.where(id: posts.map(&:id)).includes_for_post_row.order(created_at: :desc)
    @accuracy = !posts.empty? ? (posts.select(&:is_tp).size.to_f / posts.size.to_f) * 100 : 0.0
    render :preview, format: :js
  end

  def edit
    @errors = []
  end

  def update
    sites = params[:sites].present? && params[:sites].respond_to?(:map) ? params[:sites].map { |s| Site.find(s) } : []
    if @wave.update(name: params[:name], user: current_user, expiry: params[:expiry], max_flags: params[:max_flags],
                    conditions: params[:conditions], sites: sites)
      redirect_to spam_wave_path(@wave)
    else
      @errors = @wave.errors
      render :edit
    end
  end

  def cancel
    @wave.update(expiry: DateTime.now)
    flash[:success] = 'Cancelled wave.'
    redirect_back fallback_location: spam_wave_path(@wave)
  end

  def renew
    @wave.update(expiry: 48.hours.from_now)
    flash[:success] = 'Renewed wave for 48 hours.'
    redirect_back fallback_location: spam_wave_path(@wave)
  end

  private

  def set_wave
    @wave = SpamWave.find params[:id]
  end
end


text:
# frozen_string_literal: true

class DumpsController < ApplicationController
  before_action :set_dump, only: %i[show edit update destroy]

  # GET /dumps
  # GET /dumps.json
  def index
    dump_list = YAML.load_file('/var/railsapps/metasmoke/shared/dumps/files.yml')
    @dumps = dump_list['database'].map { |k, v| { name: "#{k}.sql", url: v } }
    @redis_dumps = dump_list['redis'].map { |k, v| { name: "#{k}.rdb", url: v } }
  end
end