diff --git a/lib/twitter/rest/api.rb b/lib/twitter/rest/api.rb index 98b26dbf1..e5c1d42e7 100644 --- a/lib/twitter/rest/api.rb +++ b/lib/twitter/rest/api.rb @@ -3,6 +3,7 @@ require 'twitter/rest/friends_and_followers' require 'twitter/rest/help' require 'twitter/rest/lists' +require 'twitter/rest/media' require 'twitter/rest/oauth' require 'twitter/rest/places_and_geo' require 'twitter/rest/saved_searches' @@ -23,6 +24,7 @@ module API include Twitter::REST::FriendsAndFollowers include Twitter::REST::Help include Twitter::REST::Lists + include Twitter::REST::Media include Twitter::REST::OAuth include Twitter::REST::PlacesAndGeo include Twitter::REST::SavedSearches diff --git a/lib/twitter/rest/media.rb b/lib/twitter/rest/media.rb new file mode 100644 index 000000000..d0a4f7b6b --- /dev/null +++ b/lib/twitter/rest/media.rb @@ -0,0 +1,30 @@ +require 'twitter/error' +require 'twitter/rest/utils' + +module Twitter + module REST + module Media + # Uploads media to attach to a tweet + # + # @see https://dev.twitter.com/docs/api/multiple-media-extended-entities + # @rate_limited No + # @authentication Requires user context + # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. + # @raise [Twitter::Error::UnacceptableIO] Error when the IO object for the media argument does not have a to_io method. + # @return [Integer] The uploaded media ID. + # @param media [File, Hash] A File object with your picture (PNG, JPEG or GIF) + # @param options [Hash] A customizable set of options. + # @option options [Boolean, String, Integer] :possibly_sensitive Set to true for content which may not be suitable for every audience. + def upload(media, options = {}) + fail(Twitter::Error::UnacceptableIO.new) unless media.respond_to?(:to_io) + url_prefix = 'https://upload.twitter.com' + path = '/1.1/media/upload.json' + conn = connection.dup + conn.url_prefix = url_prefix + headers = request_headers(:post, url_prefix + path, options) + options.merge!(:media => media) + conn.post(path, options) { |request| request.headers.update(headers) }.env.body[:media_id] + end + end + end +end diff --git a/lib/twitter/rest/tweets.rb b/lib/twitter/rest/tweets.rb index 3dd338429..d0484a5d1 100644 --- a/lib/twitter/rest/tweets.rb +++ b/lib/twitter/rest/tweets.rb @@ -122,6 +122,7 @@ def destroy_status(*args) # @option options [Float] :long The longitude of the location this tweet refers to. The valid ranges for longitude is -180.0 to +180.0 (East is positive) inclusive. This option will be ignored if outside that range, if it is not a number, if geo_enabled is disabled, or if there not a corresponding :lat option. # @option options [Twitter::Place] :place A place in the world. These can be retrieved from {Twitter::REST::PlacesAndGeo#reverse_geocode}. # @option options [String] :place_id A place in the world. These IDs can be retrieved from {Twitter::REST::PlacesAndGeo#reverse_geocode}. + # @option options [String] :media_ids A comma separated list of uploaded media IDs to attach to the Tweet. # @option options [String] :display_coordinates Whether or not to put a pin on the exact coordinates a tweet has been sent from. # @option options [Boolean, String, Integer] :trim_user Each tweet returned in a timeline will include a user object with only the author's numerical ID when set to true, 't' or 1. def update(status, options = {}) @@ -147,6 +148,7 @@ def update(status, options = {}) # @option options [Float] :long The longitude of the location this tweet refers to. The valid ranges for longitude is -180.0 to +180.0 (East is positive) inclusive. This option will be ignored if outside that range, if it is not a number, if geo_enabled is disabled, or if there not a corresponding :lat option. # @option options [Twitter::Place] :place A place in the world. These can be retrieved from {Twitter::REST::PlacesAndGeo#reverse_geocode}. # @option options [String] :place_id A place in the world. These IDs can be retrieved from {Twitter::REST::PlacesAndGeo#reverse_geocode}. + # @option options [String] :media_ids A comma separated list of uploaded media IDs to attach to the Tweet. # @option options [String] :display_coordinates Whether or not to put a pin on the exact coordinates a tweet has been sent from. # @option options [Boolean, String, Integer] :trim_user Each tweet returned in a timeline will include a user object with only the author's numerical ID when set to true, 't' or 1. def update!(status, options = {}) diff --git a/spec/fixtures/upload.json b/spec/fixtures/upload.json new file mode 100644 index 000000000..32845d4bc --- /dev/null +++ b/spec/fixtures/upload.json @@ -0,0 +1 @@ +{"image":{"w":428,"h":428,"image_type":"image\/png"},"media_id":470030289822314497,"media_id_string":"470030289822314497","size":68900} \ No newline at end of file diff --git a/spec/twitter/rest/media_spec.rb b/spec/twitter/rest/media_spec.rb new file mode 100644 index 000000000..9117cb3c0 --- /dev/null +++ b/spec/twitter/rest/media_spec.rb @@ -0,0 +1,54 @@ +require 'helper' + +describe Twitter::REST::Media do + before do + @client = Twitter::REST::Client.new(:consumer_key => 'CK', :consumer_secret => 'CS', :access_token => 'AT', :access_token_secret => 'AS') + end + + describe '#upload' do + before do + stub_request(:post, 'https://upload.twitter.com/1.1/media/upload.json').to_return(:body => fixture('upload.json'), :headers => {:content_type => 'application/json; charset=utf-8'}) + end + context 'a gif image' do + it 'requests the correct resource' do + @client.upload(fixture('pbjt.gif')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made + end + it 'returns an Integer' do + media_id = @client.upload(fixture('pbjt.gif')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made + expect(media_id).to be_an Integer + expect(media_id).to eq(470_030_289_822_314_497) + end + end + context 'a jpe image' do + it 'requests the correct resource' do + @client.upload(fixture('wildcomet2.jpe')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made + end + end + context 'a jpeg image' do + it 'requests the correct resource' do + @client.upload(fixture('me.jpeg')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made + end + end + context 'a png image' do + it 'requests the correct resource' do + @client.upload(fixture('we_concept_bg2.png')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made + end + end + context 'a Tempfile' do + it 'requests the correct resource' do + @client.upload(Tempfile.new('tmp')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made + end + end + context 'A non IO object' do + it 'raises an error' do + expect { @client.upload('Unacceptable IO') }.to raise_error(Twitter::Error::UnacceptableIO) + end + end + end +end diff --git a/spec/twitter/rest/tweets_spec.rb b/spec/twitter/rest/tweets_spec.rb index 9bc255cbe..91fe6840b 100644 --- a/spec/twitter/rest/tweets_spec.rb +++ b/spec/twitter/rest/tweets_spec.rb @@ -475,8 +475,7 @@ end context 'A non IO object' do it 'raises an error' do - update = lambda { @client.update_with_media('You always have options', 'Unacceptable IO') } - expect { update.call }.to raise_error(Twitter::Error::UnacceptableIO) + expect { @client.update_with_media('You always have options', 'Unacceptable IO') }.to raise_error(Twitter::Error::UnacceptableIO) end end context 'already posted' do