Skip to content

Commit

Permalink
Support DM events with media (#928)
Browse files Browse the repository at this point in the history
  • Loading branch information
FabienChaynes authored and sferik committed Jun 20, 2018
1 parent 0833471 commit 2147853
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 37 deletions.
23 changes: 23 additions & 0 deletions lib/twitter/rest/direct_messages.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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'

module Twitter
module REST
module DirectMessages
include Twitter::REST::UploadUtils
include Twitter::REST::Utils
include Twitter::Utils

Expand Down Expand Up @@ -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)
Expand Down
39 changes: 2 additions & 37 deletions lib/twitter/rest/tweets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
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'

module Twitter
module REST
module Tweets
include Twitter::REST::UploadUtils
include Twitter::REST::Utils
include Twitter::Utils
MAX_TWEETS_PER_REQUEST = 100
Expand Down Expand Up @@ -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]
Expand Down
46 changes: 46 additions & 0 deletions lib/twitter/rest/upload_utils.rb
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions spec/twitter/rest/direct_messages_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 2147853

Please sign in to comment.