sferik/twitter

View on GitHub
lib/twitter/error.rb

Summary

Maintainability
A
0 mins
Test Coverage
require "twitter/rate_limit"

module Twitter
  # Custom error class for rescuing from all Twitter errors
  class Error < StandardError
    # @return [Integer]
    attr_reader :code
    # @return [Twitter::RateLimit]
    attr_reader :rate_limit

    # Raised when Twitter returns a 4xx HTTP status code
    ClientError = Class.new(self)

    # Raised when Twitter returns the HTTP status code 400
    BadRequest = Class.new(ClientError)

    # Raised when Twitter returns the HTTP status code 401
    Unauthorized = Class.new(ClientError)

    # Raised when Twitter returns the HTTP status code 403
    Forbidden = Class.new(ClientError)

    # Raised when Twitter returns the HTTP status code 413
    RequestEntityTooLarge = Class.new(ClientError)

    # Raised when a Tweet has already been favorited
    AlreadyFavorited = Class.new(Forbidden)

    # Raised when a Tweet has already been retweeted
    AlreadyRetweeted = Class.new(Forbidden)

    # Raised when a Tweet has already been posted
    DuplicateStatus = Class.new(Forbidden)

    # Raised when Twitter returns the HTTP status code 404
    NotFound = Class.new(ClientError)

    # Raised when Twitter returns the HTTP status code 406
    NotAcceptable = Class.new(ClientError)

    # Raised when Twitter returns the HTTP status code 422
    UnprocessableEntity = Class.new(ClientError)

    # Raised when Twitter returns the HTTP status code 429
    TooManyRequests = Class.new(ClientError)

    # Raised when Twitter returns a 5xx HTTP status code
    ServerError = Class.new(self)

    # Raised when Twitter returns the HTTP status code 500
    InternalServerError = Class.new(ServerError)

    # Raised when Twitter returns the HTTP status code 502
    BadGateway = Class.new(ServerError)

    # Raised when Twitter returns the HTTP status code 503
    ServiceUnavailable = Class.new(ServerError)

    # Raised when Twitter returns the HTTP status code 504
    GatewayTimeout = Class.new(ServerError)

    # Raised when Twitter returns a media related error
    MediaError = Class.new(self)

    # Raised when Twitter returns an InvalidMedia error
    InvalidMedia = Class.new(MediaError)

    # Raised when Twitter returns a media InternalError error
    MediaInternalError = Class.new(MediaError)

    # Raised when Twitter returns an UnsupportedMedia error
    UnsupportedMedia = Class.new(MediaError)

    # Raised when an operation subject to timeout takes too long
    TimeoutError = Class.new(self)

    ERRORS = {
      400 => Twitter::Error::BadRequest,
      401 => Twitter::Error::Unauthorized,
      403 => Twitter::Error::Forbidden,
      404 => Twitter::Error::NotFound,
      406 => Twitter::Error::NotAcceptable,
      413 => Twitter::Error::RequestEntityTooLarge,
      420 => Twitter::Error::TooManyRequests,
      422 => Twitter::Error::UnprocessableEntity,
      429 => Twitter::Error::TooManyRequests,
      500 => Twitter::Error::InternalServerError,
      502 => Twitter::Error::BadGateway,
      503 => Twitter::Error::ServiceUnavailable,
      504 => Twitter::Error::GatewayTimeout,
    }.freeze

    FORBIDDEN_MESSAGES = proc do |message|
      case message
      when /(?=.*status).*duplicate/i
        # - "Status is a duplicate."
        Twitter::Error::DuplicateStatus
      when /already favorited/i
        # - "You have already favorited this status."
        Twitter::Error::AlreadyFavorited
      when /already retweeted|Share validations failed/i
        # - "You have already retweeted this Tweet." (Nov 2017-)
        # - "You have already retweeted this tweet." (?-Nov 2017)
        # - "sharing is not permissible for this status (Share validations failed)" (-? 2017)
        Twitter::Error::AlreadyRetweeted
      end
    end

    MEDIA_ERRORS = {
      "InternalError" => Twitter::Error::MediaInternalError,
      "InvalidMedia" => Twitter::Error::InvalidMedia,
      "UnsupportedMedia" => Twitter::Error::UnsupportedMedia,
    }.freeze

    # If error code is missing see https://developer.twitter.com/en/docs/basics/response-codes
    module Code
      AUTHENTICATION_PROBLEM       =  32
      RESOURCE_NOT_FOUND           =  34
      SUSPENDED_ACCOUNT            =  64
      DEPRECATED_CALL              =  68
      RATE_LIMIT_EXCEEDED          =  88
      INVALID_OR_EXPIRED_TOKEN     =  89
      SSL_REQUIRED                 =  92
      UNABLE_TO_VERIFY_CREDENTIALS =  99
      OVER_CAPACITY                = 130
      INTERNAL_ERROR               = 131
      OAUTH_TIMESTAMP_OUT_OF_RANGE = 135
      ALREADY_FAVORITED            = 139
      FOLLOW_ALREADY_REQUESTED     = 160
      FOLLOW_LIMIT_EXCEEDED        = 161
      PROTECTED_STATUS             = 179
      OVER_UPDATE_LIMIT            = 185
      DUPLICATE_STATUS             = 187
      BAD_AUTHENTICATION_DATA      = 215
      SPAM                         = 226
      LOGIN_VERIFICATION_NEEDED    = 231
      ENDPOINT_RETIRED             = 251
      CANNOT_WRITE                 = 261
      CANNOT_MUTE                  = 271
      CANNOT_UNMUTE                = 272
    end

    class << self
      include Twitter::Utils

      # Create a new error from an HTTP response
      #
      # @param body [String]
      # @param headers [Hash]
      # @return [Twitter::Error]
      def from_response(body, headers)
        message, code = parse_error(body)
        new(message, headers, code)
      end

      # Create a new error from a media error hash
      #
      # @param error [Hash]
      # @param headers [Hash]
      # @return [Twitter::MediaError]
      def from_processing_response(error, headers)
        klass = MEDIA_ERRORS[error[:name]] || self
        message = error[:message]
        code = error[:code]
        klass.new(message, headers, code)
      end

    private

      def parse_error(body)
        if body.nil? || body.empty?
          ["", nil]
        elsif body[:error]
          [body[:error], nil]
        elsif body[:errors]
          extract_message_from_errors(body)
        end
      end

      def extract_message_from_errors(body)
        first = Array(body[:errors]).first
        if first.is_a?(Hash)
          [first[:message].chomp, first[:code]]
        else
          [first.chomp, nil]
        end
      end
    end

    # Initializes a new Error object
    #
    # @param message [Exception, String]
    # @param rate_limit [Hash]
    # @param code [Integer]
    # @return [Twitter::Error]
    def initialize(message = "", rate_limit = {}, code = nil)
      super(message)
      @rate_limit = Twitter::RateLimit.new(rate_limit)
      @code = code
    end
  end
end