sferik/twitter

View on GitHub
lib/twitter/rest/upload_utils.rb

Summary

Maintainability
A
0 mins
Test Coverage
require "twitter/rest/request"

module Twitter
  module REST
    module UploadUtils
    private

      # Uploads images and videos. Videos require multiple requests and uploads in chunks of 5 Megabytes.
      # The only supported video format is mp4.
      #
      # @see https://developer.twitter.com/en/docs/media/upload-media/uploading-media/media-best-practices
      def upload(media, media_category_prefix: "tweet")
        ext = File.extname(media)
        return chunk_upload(media, "video/mp4", "#{media_category_prefix}_video") if ext == ".mp4"
        return chunk_upload(media, "video/quicktime", "#{media_category_prefix}_video") if ext == ".mov"
        return chunk_upload(media, "image/gif", "#{media_category_prefix}_gif") if ext == ".gif" && File.size(media) > 5_000_000

        Twitter::REST::Request.new(self, :multipart_post, "https://upload.twitter.com/1.1/media/upload.json", key: :media, file: media).perform
      end

      # @raise [Twitter::Error::TimeoutError] Error raised when the upload is longer than the value specified in Twitter::Client#timeouts[:upload].
      # @raise [Twitter::Error::MediaError] Error raised when Twitter return an error about a media which is not mapped by the gem.
      # @raise [Twitter::Error::MediaInternalError] Error raised when Twitter returns an InternalError error.
      # @raise [Twitter::Error::InvalidMedia] Error raised when Twitter returns an InvalidMedia error.
      # @raise [Twitter::Error::UnsupportedMedia] Error raised when Twitter returns an UnsupportedMedia error.
      # @see https://developer.twitter.com/en/docs/media/upload-media/uploading-media/chunked-media-upload
      def chunk_upload(media, media_type, media_category)
        Timeout.timeout(timeouts&.fetch(:upload, nil), Twitter::Error::TimeoutError) do
          init = Twitter::REST::Request.new(self, :post, "https://upload.twitter.com/1.1/media/upload.json",
                                            command: "INIT",
                                            media_type:,
                                            media_category:,
                                            total_bytes: media.size).perform
          append_media(media, init[:media_id])
          media.close
          finalize_media(init[:media_id])
        end
      end

      # @see https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-append
      def append_media(media, media_id)
        until media.eof?
          chunk = media.read(5_000_000)
          seg ||= -1
          Twitter::REST::Request.new(self, :multipart_post, "https://upload.twitter.com/1.1/media/upload.json",
                                     command: "APPEND",
                                     media_id:,
                                     segment_index: seg += 1,
                                     key: :media,
                                     file: StringIO.new(chunk)).perform
        end
      end

      # @see https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-finalize
      # @see https://developer.twitter.com/en/docs/media/upload-media/api-reference/get-media-upload-status
      def finalize_media(media_id)
        response = Twitter::REST::Request.new(self, :post, "https://upload.twitter.com/1.1/media/upload.json",
                                              command: "FINALIZE", media_id:).perform
        failed_or_succeeded = %w[failed succeeded]

        loop do
          return response if !response[:processing_info] || failed_or_succeeded.include?(response[:processing_info][:state])

          sleep(response[:processing_info][:check_after_secs])
          response = Twitter::REST::Request.new(self, :get, "https://upload.twitter.com/1.1/media/upload.json",
                                                command: "STATUS", media_id:).perform
        end
        response
      end
    end
  end
end