diff --git a/lib/twitter/rest/direct_messages.rb b/lib/twitter/rest/direct_messages.rb index 362bd5e51..6561e2992 100644 --- a/lib/twitter/rest/direct_messages.rb +++ b/lib/twitter/rest/direct_messages.rb @@ -1,6 +1,7 @@ require 'twitter/arguments' require 'twitter/direct_message' require 'twitter/direct_message_event' +require 'twitter/rest/upload_utils' require 'twitter/rest/utils' require 'twitter/user' require 'twitter/utils' @@ -8,6 +9,7 @@ module Twitter module REST module DirectMessages + include Twitter::REST::UploadUtils include Twitter::REST::Utils include Twitter::Utils @@ -192,6 +194,27 @@ def create_direct_message_event(*args) Twitter::DirectMessageEvent.new(response[:event]) end + # Create a new direct message event to the specified user from the authenticating user with media + # + # @see https://developer.twitter.com/en/docs/direct-messages/sending-and-receiving/api-reference/new-event + # @see https://developer.twitter.com/en/docs/direct-messages/message-attachments/guides/attaching-media + # @note This method requires an access token with RWD (read, write & direct message) permissions. Consult The Application Permission Model for more information. + # @rate_limited Yes + # @authentication Requires user context + # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid. + # @return [Twitter::DirectMessageEvent] The created direct message event. + # @param user [Integer, String, Twitter::User] A Twitter user ID, screen name, URI, or object. + # @param text [String] The text of your direct message, up to 10,000 characters. + # @param media [File] A media file (PNG, JPEG, GIF or MP4). + # @param options [Hash] A customizable set of options. + def create_direct_message_event_with_media(user, text, media, options = {}) + media_id = upload(media, media_category_prefix: 'dm')[:media_id] + options = options.dup + options[:event] = {type: 'message_create', message_create: {target: {recipient_id: extract_id(user)}, message_data: {text: text, attachment: {type: 'media', media: {id: media_id}}}}} + response = Twitter::REST::Request.new(self, :json_post, '/1.1/direct_messages/events/new.json', options).perform + Twitter::DirectMessageEvent.new(response[:event]) + end + private def format_json_options(user_id, text, options) diff --git a/lib/twitter/rest/tweets.rb b/lib/twitter/rest/tweets.rb index e28644a5a..6fb21713b 100644 --- a/lib/twitter/rest/tweets.rb +++ b/lib/twitter/rest/tweets.rb @@ -2,6 +2,7 @@ require 'twitter/error' require 'twitter/oembed' require 'twitter/rest/request' +require 'twitter/rest/upload_utils' require 'twitter/rest/utils' require 'twitter/tweet' require 'twitter/utils' @@ -9,6 +10,7 @@ module Twitter module REST module Tweets + include Twitter::REST::UploadUtils include Twitter::REST::Utils include Twitter::Utils MAX_TWEETS_PER_REQUEST = 100 @@ -323,43 +325,6 @@ def unretweet(*args) 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://dev.twitter.com/rest/public/uploading-media - def upload(media) - return chunk_upload(media, 'video/mp4', 'tweet_video') if File.extname(media) == '.mp4' - return chunk_upload(media, 'image/gif', 'tweet_gif') if File.extname(media) == '.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 - - # rubocop:disable MethodLength - def chunk_upload(media, media_type, media_category) - init = Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json', - command: 'INIT', - media_type: media_type, - media_category: media_category, - total_bytes: media.size).perform - - 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: init[:media_id], - segment_index: seg += 1, - key: :media, - file: StringIO.new(chunk)).perform - end - - media.close - - Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json', - command: 'FINALIZE', media_id: init[:media_id]).perform - end - # rubocop:enable MethodLength - def array_wrap(object) if object.respond_to?(:to_ary) object.to_ary || [object] diff --git a/lib/twitter/rest/upload_utils.rb b/lib/twitter/rest/upload_utils.rb new file mode 100644 index 000000000..d13839744 --- /dev/null +++ b/lib/twitter/rest/upload_utils.rb @@ -0,0 +1,46 @@ +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://dev.twitter.com/rest/public/uploading-media + def upload(media, media_category_prefix: 'tweet') + return chunk_upload(media, 'video/mp4', "#{media_category_prefix}_video") if File.extname(media) == '.mp4' + return chunk_upload(media, 'image/gif', "#{media_category_prefix}_gif") if File.extname(media) == '.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 + + # rubocop:disable MethodLength + def chunk_upload(media, media_type, media_category) + init = Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json', + command: 'INIT', + media_type: media_type, + media_category: media_category, + total_bytes: media.size).perform + + 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: init[:media_id], + segment_index: seg += 1, + key: :media, + file: StringIO.new(chunk)).perform + end + + media.close + + Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json', + command: 'FINALIZE', media_id: init[:media_id]).perform + end + # rubocop:enable MethodLength + end + end +end diff --git a/spec/twitter/rest/direct_messages_spec.rb b/spec/twitter/rest/direct_messages_spec.rb index 688a174b3..5515f3f43 100644 --- a/spec/twitter/rest/direct_messages_spec.rb +++ b/spec/twitter/rest/direct_messages_spec.rb @@ -165,4 +165,74 @@ expect(direct_message_event.direct_message.text).to eq('testing') end end + + describe '#create_direct_message_event_with_media' do + before do + stub_post('/1.1/direct_messages/events/new.json').to_return(body: fixture('direct_message_event.json'), headers: {content_type: 'application/json; charset=utf-8'}) + 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 'with a gif image' do + it 'requests the correct resource' do + @client.create_direct_message_event_with_media(58_983, 'testing', fixture('pbjt.gif')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made + expect(a_post('/1.1/direct_messages/events/new.json')).to have_been_made + end + it 'returns a DirectMessageEvent' do + direct_message_event = @client.create_direct_message_event_with_media(58_983, 'testing', fixture('pbjt.gif')) + expect(direct_message_event).to be_a Twitter::DirectMessageEvent + expect(direct_message_event.direct_message.text).to eq('testing') + end + context 'which size is bigger than 5 megabytes' do + let(:big_gif) { fixture('pbjt.gif') } + before do + expect(File).to receive(:size).with(big_gif).and_return(7_000_000) + end + it 'requests the correct resource' do + @client.create_direct_message_event_with_media(58_983, 'testing', big_gif) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made.times(3) + expect(a_post('/1.1/direct_messages/events/new.json')).to have_been_made + end + it 'returns a DirectMessageEvent' do + direct_message_event = @client.create_direct_message_event_with_media(58_983, 'testing', big_gif) + expect(direct_message_event).to be_a Twitter::DirectMessageEvent + expect(direct_message_event.direct_message.text).to eq('testing') + end + end + end + context 'with a jpe image' do + it 'requests the correct resource' do + @client.create_direct_message_event_with_media(58_983, 'You always have options', fixture('wildcomet2.jpe')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made + expect(a_post('/1.1/direct_messages/events/new.json')).to have_been_made + end + end + context 'with a jpeg image' do + it 'requests the correct resource' do + @client.create_direct_message_event_with_media(58_983, 'You always have options', fixture('me.jpeg')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made + expect(a_post('/1.1/direct_messages/events/new.json')).to have_been_made + end + end + context 'with a png image' do + it 'requests the correct resource' do + @client.create_direct_message_event_with_media(58_983, 'You always have options', fixture('we_concept_bg2.png')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made + expect(a_post('/1.1/direct_messages/events/new.json')).to have_been_made + end + end + context 'with a mp4 video' do + it 'requests the correct resources' do + @client.create_direct_message_event_with_media(58_983, 'You always have options', fixture('1080p.mp4')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made.times(3) + expect(a_post('/1.1/direct_messages/events/new.json')).to have_been_made + end + end + context 'with a Tempfile' do + it 'requests the correct resource' do + @client.create_direct_message_event_with_media(58_983, 'You always have options', Tempfile.new('tmp')) + expect(a_request(:post, 'https://upload.twitter.com/1.1/media/upload.json')).to have_been_made + expect(a_post('/1.1/direct_messages/events/new.json')).to have_been_made + end + end + end end