bikeindex/bike_index

View on GitHub
app/models/tweet.rb

Summary

Maintainability
A
2 hrs
Test Coverage
D
67%
class Tweet < ApplicationRecord
  KIND_ENUM = {stolen_tweet: 0, imported_tweet: 1, app_tweet: 2}.freeze
  VALID_ALIGNMENTS = %w[top-left top-right bottom-left bottom-right].freeze
  validates :twitter_id, uniqueness: true, allow_blank: true
  has_many :public_images, as: :imageable, dependent: :destroy

  belongs_to :twitter_account
  belongs_to :stolen_record

  belongs_to :original_tweet, class_name: "Tweet"
  has_many :retweets,
    foreign_key: :original_tweet_id,
    class_name: "Tweet",
    dependent: :destroy

  mount_uploader :image, ImageUploader

  before_validation :set_calculated_attributes

  enum kind: KIND_ENUM

  scope :retweet, -> { where.not(original_tweet: nil) }
  scope :not_retweet, -> { where(original_tweet: nil) }
  scope :not_stolen, -> { where.not(kind: "stolen_tweet") }

  def self.kinds
    KIND_ENUM.keys.map(&:to_s)
  end

  def self.friendly_find(id)
    return nil if id.blank?
    id = id.to_s
    query = id.length > 15 ? {twitter_id: id} : {id: id}
    order(created_at: :desc).find_by(query)
  end

  def self.auto_link_text(text)
    text.gsub(/@([^\s])*/) {
      username = Regexp.last_match[0]
      "<a href=\"https://twitter.com/#{username.delete("@")}\" target=\"_blank\">#{username}</a>"
    }.gsub(/#([^\s])*/) do
      hashtag = Regexp.last_match[0]
      "<a href=\"https://twitter.com/hashtag/#{hashtag.delete("#")}\" target=\"_blank\">#{hashtag}</a>"
    end
  end

  def self.admin_search(str)
    return none unless str.present?
    text = str.strip
    # If passed a number, assume it is a bike ID and search for that bike_id
    if text.is_a?(Integer) || text.match(/\A\d+\z/).present?
      if text.to_i > 2147483647 # max rails integer, assume it's a twitter_id instead
        return where("twitter_id ILIKE ?", "%#{text}%")
      else
        return includes(:stolen_record).where(stolen_records: {bike_id: text})
      end
    end
    where("body_html ILIKE ?", "%#{text}%").or(where("body ILIKE ?", "%#{text}%"))
  end

  # TODO: Add actual testing of this. It isn't tested right now, sorry :/
  def send_tweet
    return true unless app_tweet? && twitter_response.blank?
    if image.present?
      Tempfile.open("foto.jpg") do |foto|
        foto.binmode
        foto.write open_image.read # TODO: Refactor this.
        foto.rewind
        tweeted = twitter_account.tweet(body, foto)
        update(twitter_response: tweeted.as_json)
      end
    else
      tweeted = twitter_account.tweet(body)
      update(twitter_response: tweeted.as_json)
    end
    tweeted
  end

  # TODO: Add actual testing of this. It isn't tested right now, sorry :/
  def retweet_to_account(retweet_account)
    return nil if retweet_account.id.to_i == twitter_account_id.to_i
    posted_retweet = retweet_account.retweet(twitter_id)
    return nil if posted_retweet.blank?

    retweet = Tweet.new(
      twitter_id: posted_retweet.id,
      twitter_account_id: retweet_account.id,
      stolen_record_id: stolen_record_id,
      original_tweet_id: id
    )

    unless retweet.save
      retweet_account.set_error(retweet.errors.full_messages.to_sentence)
    end
    retweet
  end

  # Because of recoveries
  def stolen_record
    return nil unless stolen_record_id.present?
    # Using super because maybe it will benefit from includes?
    super || StolenRecord.current_and_not.find(stolen_record_id)
  end

  def bike
    stolen_record&.bike
  end

  def retweet?
    original_tweet.present?
  end

  def to_param
    twitter_id
  end

  def set_calculated_attributes
    self.kind ||= calculated_kind
    if imported_tweet?
      self.body_html ||= self.class.auto_link_text(trh[:text]) if trh.dig(:text).present?
      self.alignment ||= VALID_ALIGNMENTS.first
      unless VALID_ALIGNMENTS.include?(alignment)
        errors[:base] << "#{alignment} is not one of valid alignments: #{VALID_ALIGNMENTS}"
      end
    else
      if kind == "app_tweet" && twitter_id.blank?
        errors[:base] << "You need to choose an account" unless twitter_account.present?
        errors[:base] << "You need to include tweet text" unless body.present?
      end
      self.twitter_id ||= trh[:id]
      self.body ||= tweeted_text
    end
  end

  def trh
    (twitter_response || {}).with_indifferent_access
  end

  def tweeted_at
    TimeParser.parse(trh[:created_at])
  end

  def tweeted_image
    return nil unless trh.dig(:entities, :media).present?
    trh.dig(:entities, :media).first&.dig(:media_url_https)
  end

  def tweeted_text
    trh[:text]
  end

  def tweetor
    return twitter_account.screen_name if twitter_account&.screen_name.present?
    trh.dig(:user, :screen_name)
  end

  def tweetor_avatar
    trh.dig(:user, :profile_image_url_https)
  end

  def tweetor_name
    trh.dig(:user, :name)
  end

  def tweetor_link
    twitter_account&.twitter_account_url || "https://twitter.com/#{tweetor}"
  end

  def tweet_link
    if twitter_account.present?
      [twitter_account.twitter_account_url, "status", twitter_id].join("/")
    else
      "https://twitter.com/#{tweetor}/status/#{twitter_id}"
    end
  end

  def details_hash
    @details_hash ||= {}.tap do |details|
      details[:notification_type] = "stolen_twitter_alerter"
      details[:bike_id] = bike&.id
      details[:tweet_id] = twitter_id
      details[:tweet_string] = body_html
      details[:tweet_account_screen_name] = tweetor
      details[:tweet_account_name] = twitter_account&.account_info_name
      details[:tweet_account_image] = twitter_account&.account_info_image
      details[:retweet_screen_names] = retweets.map(&:tweetor)

      if !twitter_account&.national? && twitter_account&.address_string.present?
        details[:location] = twitter_account.address_string.split(",").first.strip
      end
    end
  end

  # Because the way we load the image is different if it's remote or local
  # This is hacky, but whatever. Copied from bulk_import
  def open_image
    local_image = image&._storage&.to_s == "CarrierWave::Storage::File"
    local_image ? File.open(image.path, "r") : URI.parse(image.url).open
  end

  private

  def calculated_kind
    return "stolen_tweet" if stolen_record_id.present?
    return "imported_tweet" if twitter_id.present?
    "app_tweet"
  end
end