view app/models/tweet.rb @ 243:bc2f45058c9e legit-client

Prevent caching of rate limited error and combine response handling
author nanaya <me@nanaya.net>
date Sun, 16 Jul 2023 08:53:59 +0900
parents 545ce38ef3d6
children
line wrap: on
line source

class Tweet
  TIMELINE_OPTIONS = {
    :count => 100,
    :exclude_replies => false,
    :include_rts => true,
    :tweet_mode => :extended,
  }

  def self.cache_expires_time
    (15 + rand(15)).minutes
  end

  def initialize(twitter_id)
    @twitter_id = twitter_id
  end

  def id
    user.id
  end

  def timeline
    if @timeline.nil?
      cache_key = "timeline:v2:#{id}/#{Base64.urlsafe_encode64 id.to_s}"
      raw = Rails.cache.fetch(cache_key, :expires_in => self.class.cache_expires_time) do
        client_try(:user_timeline, id, TIMELINE_OPTIONS).tap do |data|
          if data[:result] == :ok
            if data[:data].any? && data[:data].first.user.id != id
              wrong_user = data[:data].first.user
              throw "Wrong timeline data. Requested: #{id}, got: #{wrong_user.id} (#{wrong_user.screen_name.printable})"
            end

            data[:data] = data[:data].select do |tweet|
              tweet.retweeted_status.nil? || tweet.user.id != tweet.retweeted_status.user.id
            end.map { |tweet| tweet.to_h }
          end
        end
      end

      raise Twitter::Error::NotFound if raw[:result] == :not_found

      @timeline = raw[:data].map { |tweet_hash| Twitter::Tweet.new(tweet_hash) }
    end

    @timeline
  end

  def user
    if @user.nil?
      cache_key = "user:v1:#{@twitter_id.is_a?(Integer) ? 'id' : 'lookup'}:#{@twitter_id}"
      raw = Rails.cache.fetch(cache_key, :expires_in => self.class.cache_expires_time) do
        client_try(:user, @twitter_id).tap do |data|
          if data[:result] == :ok
            user = data[:data]

            if user.id != @twitter_id && user.screen_name.downcase != @twitter_id.try(:downcase)
              throw "Wrong user data. Requested: #{@twitter_id}, got: #{user.id} (#{user.screen_name.printable})"
            end
          end
        end
      end

      raise Twitter::Error::NotFound if raw[:result] == :not_found

      @user = raw[:data]
    end

    @user
  end

  def client
    Clients.instance.get client_config_id
  end

  def client_try(method, *args)
    initial_config_id = client_config_id

    begin
      data = client.public_send method, *args
    rescue Twitter::Error::TooManyRequests
      @client_config_id = (1 + @client_config_id) % @client_config_count

      if initial_config_id == client_config_id
        raise
      else
        retry
      end
    rescue Twitter::Error::NotFound
      return { :result => :not_found }
    end

    { :result => :ok, :data => data }
  end

  def client_config_id
    @client_config_count ||= $cfg[:twitter].size
    @client_config_id ||= rand(@client_config_count)

    @client_config_id
  end
end