twitter 6.2.0 → 8.0.0.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -10
  3. data/LICENSE.md +1 -1
  4. data/README.md +9 -14
  5. data/lib/twitter/base.rb +11 -11
  6. data/lib/twitter/basic_user.rb +3 -2
  7. data/lib/twitter/client.rb +8 -8
  8. data/lib/twitter/creatable.rb +7 -3
  9. data/lib/twitter/cursor.rb +15 -5
  10. data/lib/twitter/direct_message.rb +4 -3
  11. data/lib/twitter/direct_message_event.rb +44 -0
  12. data/lib/twitter/direct_messages/welcome_message.rb +17 -0
  13. data/lib/twitter/direct_messages/welcome_message_rule.rb +12 -0
  14. data/lib/twitter/direct_messages/welcome_message_rule_wrapper.rb +36 -0
  15. data/lib/twitter/direct_messages/welcome_message_wrapper.rb +42 -0
  16. data/lib/twitter/entities.rb +6 -6
  17. data/lib/twitter/entity/hashtag.rb +1 -1
  18. data/lib/twitter/entity/symbol.rb +1 -1
  19. data/lib/twitter/entity/uri.rb +1 -1
  20. data/lib/twitter/entity/user_mention.rb +1 -1
  21. data/lib/twitter/entity.rb +1 -1
  22. data/lib/twitter/enumerable.rb +15 -6
  23. data/lib/twitter/error.rb +52 -9
  24. data/lib/twitter/factory.rb +1 -1
  25. data/lib/twitter/geo/point.rb +1 -1
  26. data/lib/twitter/geo/polygon.rb +1 -1
  27. data/lib/twitter/geo.rb +2 -2
  28. data/lib/twitter/geo_factory.rb +3 -3
  29. data/lib/twitter/geo_results.rb +2 -2
  30. data/lib/twitter/headers.rb +4 -4
  31. data/lib/twitter/identity.rb +2 -2
  32. data/lib/twitter/language.rb +1 -1
  33. data/lib/twitter/list.rb +3 -2
  34. data/lib/twitter/media/animated_gif.rb +1 -1
  35. data/lib/twitter/media/photo.rb +2 -2
  36. data/lib/twitter/media/video.rb +3 -3
  37. data/lib/twitter/media/video_info.rb +2 -2
  38. data/lib/twitter/media_factory.rb +4 -4
  39. data/lib/twitter/metadata.rb +1 -1
  40. data/lib/twitter/null_object.rb +19 -5
  41. data/lib/twitter/oembed.rb +2 -1
  42. data/lib/twitter/place.rb +3 -3
  43. data/lib/twitter/premium_search_results.rb +67 -0
  44. data/lib/twitter/profile.rb +6 -6
  45. data/lib/twitter/profile_banner.rb +2 -2
  46. data/lib/twitter/rate_limit.rb +6 -6
  47. data/lib/twitter/relationship.rb +2 -1
  48. data/lib/twitter/rest/account_activity.rb +99 -0
  49. data/lib/twitter/rest/api.rb +22 -16
  50. data/lib/twitter/rest/client.rb +4 -4
  51. data/lib/twitter/rest/direct_messages/welcome_messages.rb +90 -0
  52. data/lib/twitter/rest/direct_messages.rb +136 -47
  53. data/lib/twitter/rest/favorites.rb +17 -21
  54. data/lib/twitter/rest/form_encoder.rb +27 -0
  55. data/lib/twitter/rest/friends_and_followers.rb +21 -21
  56. data/lib/twitter/rest/help.rb +6 -18
  57. data/lib/twitter/rest/lists.rb +33 -32
  58. data/lib/twitter/rest/oauth.rb +8 -8
  59. data/lib/twitter/rest/places_and_geo.rb +6 -6
  60. data/lib/twitter/rest/premium_search.rb +34 -0
  61. data/lib/twitter/rest/request.rb +64 -31
  62. data/lib/twitter/rest/saved_searches.rb +6 -6
  63. data/lib/twitter/rest/search.rb +6 -5
  64. data/lib/twitter/rest/spam_reporting.rb +3 -3
  65. data/lib/twitter/rest/suggested_users.rb +5 -5
  66. data/lib/twitter/rest/timelines.rb +8 -7
  67. data/lib/twitter/rest/trends.rb +7 -7
  68. data/lib/twitter/rest/tweets.rb +20 -53
  69. data/lib/twitter/rest/undocumented.rb +7 -7
  70. data/lib/twitter/rest/upload_utils.rb +68 -0
  71. data/lib/twitter/rest/users.rb +32 -32
  72. data/lib/twitter/rest/utils.rb +41 -24
  73. data/lib/twitter/saved_search.rb +2 -2
  74. data/lib/twitter/search_results.rb +10 -9
  75. data/lib/twitter/settings.rb +2 -1
  76. data/lib/twitter/size.rb +2 -2
  77. data/lib/twitter/source_user.rb +1 -1
  78. data/lib/twitter/streaming/client.rb +19 -15
  79. data/lib/twitter/streaming/connection.rb +19 -7
  80. data/lib/twitter/streaming/message_parser.rb +7 -7
  81. data/lib/twitter/streaming/response.rb +12 -10
  82. data/lib/twitter/suggestion.rb +3 -3
  83. data/lib/twitter/target_user.rb +1 -1
  84. data/lib/twitter/trend.rb +3 -2
  85. data/lib/twitter/trend_results.rb +5 -5
  86. data/lib/twitter/tweet.rb +13 -4
  87. data/lib/twitter/user.rb +9 -9
  88. data/lib/twitter/utils.rb +6 -4
  89. data/lib/twitter/variant.rb +2 -1
  90. data/lib/twitter/version.rb +4 -4
  91. data/lib/twitter.rb +36 -31
  92. data/twitter.gemspec +19 -19
  93. metadata +33 -37
  94. data/lib/twitter/configuration.rb +0 -27
@@ -1,18 +1,19 @@
1
- require 'addressable/uri'
2
- require 'http'
3
- require 'http/form_data'
4
- require 'json'
5
- require 'openssl'
6
- require 'twitter/error'
7
- require 'twitter/headers'
8
- require 'twitter/rate_limit'
9
- require 'twitter/utils'
1
+ require "addressable/uri"
2
+ require "http"
3
+ require "http/form_data"
4
+ require "json"
5
+ require "openssl"
6
+ require "twitter/error"
7
+ require "twitter/headers"
8
+ require "twitter/rate_limit"
9
+ require "twitter/utils"
10
+ require "twitter/rest/form_encoder"
10
11
 
11
12
  module Twitter
12
13
  module REST
13
- class Request
14
+ class Request # rubocop:disable Metrics/ClassLength
14
15
  include Twitter::Utils
15
- BASE_URL = 'https://api.twitter.com'.freeze
16
+ BASE_URL = "https://api.twitter.com".freeze
16
17
  attr_accessor :client, :headers, :options, :path, :rate_limit,
17
18
  :request_method, :uri
18
19
  alias verb request_method
@@ -22,63 +23,85 @@ module Twitter
22
23
  # @param path [String]
23
24
  # @param options [Hash]
24
25
  # @return [Twitter::REST::Request]
25
- def initialize(client, request_method, path, options = {})
26
+ def initialize(client, request_method, path, options = {}, params = nil)
26
27
  @client = client
27
- @uri = Addressable::URI.parse(path.start_with?('http') ? path : BASE_URL + path)
28
- set_multipart_options!(request_method, options)
28
+ @uri = Addressable::URI.parse(path.start_with?("http") ? path : BASE_URL + path)
29
+ multipart_options = params || options
30
+ set_multipart_options!(request_method, multipart_options)
29
31
  @path = uri.path
30
32
  @options = options
33
+ @options_key = {get: :params, json_post: :json, json_put: :json, delete: :params}[request_method] || :form
34
+ @params = params
31
35
  end
32
36
 
33
37
  # @return [Array, Hash]
34
38
  def perform
35
- options_key = @request_method == :get ? :params : :form
36
- response = http_client.headers(@headers).public_send(@request_method, @uri.to_s, options_key => @options)
37
- response_body = response.body.empty? ? '' : symbolize_keys!(response.parse)
39
+ response = http_client.headers(@headers).public_send(@request_method, @uri.to_s, request_options)
40
+ response_body = response.body.empty? ? "" : symbolize_keys!(response.parse)
38
41
  response_headers = response.headers
39
42
  fail_or_return_response_body(response.code, response_body, response_headers)
40
43
  end
41
44
 
42
45
  private
43
46
 
47
+ def request_options
48
+ options = if @options_key == :form
49
+ {form: HTTP::FormData.create(@options, encoder: Twitter::REST::FormEncoder.method(:encode))}
50
+ else
51
+ {@options_key => @options}
52
+ end
53
+
54
+ if @params
55
+ if options[:params]
56
+ options[:params].merge(@params)
57
+ else
58
+ options[:params] = @params
59
+ end
60
+ end
61
+ options
62
+ end
63
+
44
64
  def merge_multipart_file!(options)
45
65
  key = options.delete(:key)
46
66
  file = options.delete(:file)
47
67
 
48
68
  options[key] = if file.is_a?(StringIO)
49
- HTTP::FormData::File.new(file, mime_type: 'video/mp4')
69
+ HTTP::FormData::File.new(file, content_type: "video/mp4")
50
70
  else
51
- HTTP::FormData::File.new(file, filename: File.basename(file), mime_type: mime_type(File.basename(file)))
71
+ HTTP::FormData::File.new(file, filename: File.basename(file), content_type: content_type(File.basename(file)))
52
72
  end
53
73
  end
54
74
 
55
75
  def set_multipart_options!(request_method, options)
56
- if request_method == :multipart_post
57
- merge_multipart_file!(options)
76
+ if %i[multipart_post json_post].include?(request_method)
77
+ merge_multipart_file!(options) if request_method == :multipart_post
78
+ options = {}
58
79
  @request_method = :post
59
- @headers = Twitter::Headers.new(@client, @request_method, @uri).request_headers
80
+ elsif request_method == :json_put
81
+ @request_method = :put
60
82
  else
61
83
  @request_method = request_method
62
- @headers = Twitter::Headers.new(@client, @request_method, @uri, options).request_headers
63
84
  end
85
+ @headers = Twitter::Headers.new(@client, @request_method, @uri, options).request_headers
64
86
  end
65
87
 
66
- def mime_type(basename)
88
+ def content_type(basename)
67
89
  case basename
68
90
  when /\.gif$/i
69
- 'image/gif'
91
+ "image/gif"
70
92
  when /\.jpe?g/i
71
- 'image/jpeg'
93
+ "image/jpeg"
72
94
  when /\.png$/i
73
- 'image/png'
95
+ "image/png"
74
96
  else
75
- 'application/octet-stream'
97
+ "application/octet-stream"
76
98
  end
77
99
  end
78
100
 
79
101
  def fail_or_return_response_body(code, body, headers)
80
102
  error = error(code, body, headers)
81
103
  raise(error) if error
104
+
82
105
  @rate_limit = Twitter::RateLimit.new(headers)
83
106
  body
84
107
  end
@@ -89,6 +112,8 @@ module Twitter
89
112
  forbidden_error(body, headers)
90
113
  elsif !klass.nil?
91
114
  klass.from_response(body, headers)
115
+ elsif body.is_a?(Hash) && (err = body.dig(:processing_info, :error))
116
+ Twitter::Error::MediaError.from_processing_response(err, headers)
92
117
  end
93
118
  end
94
119
 
@@ -103,11 +128,12 @@ module Twitter
103
128
  end
104
129
 
105
130
  def symbolize_keys!(object)
106
- if object.is_a?(Array)
131
+ case object
132
+ when Array
107
133
  object.each_with_index do |val, index|
108
134
  object[index] = symbolize_keys!(val)
109
135
  end
110
- elsif object.is_a?(Hash)
136
+ when Hash
111
137
  object.dup.each_key do |key|
112
138
  object[key.to_sym] = symbolize_keys!(object.delete(key))
113
139
  end
@@ -115,10 +141,17 @@ module Twitter
115
141
  object
116
142
  end
117
143
 
144
+ # Returns boolean indicating if all the keys required by HTTP::Client are present in Twitter::Client#timeouts
145
+ #
146
+ # @return [Boolean]
147
+ def timeout_keys_defined
148
+ (%i[write connect read] - (@client.timeouts&.keys || [])).empty?
149
+ end
150
+
118
151
  # @return [HTTP::Client, HTTP]
119
152
  def http_client
120
153
  client = @client.proxy ? HTTP.via(*proxy) : HTTP
121
- client = client.timeout(:per_operation, connect: @client.timeouts[:connect], read: @client.timeouts[:read], write: @client.timeouts[:write]) if @client.timeouts
154
+ client = client.timeout(connect: @client.timeouts[:connect], read: @client.timeouts[:read], write: @client.timeouts[:write]) if timeout_keys_defined
122
155
  client
123
156
  end
124
157
 
@@ -1,7 +1,7 @@
1
- require 'twitter/arguments'
2
- require 'twitter/rest/utils'
3
- require 'twitter/saved_search'
4
- require 'twitter/utils'
1
+ require "twitter/arguments"
2
+ require "twitter/rest/utils"
3
+ require "twitter/saved_search"
4
+ require "twitter/utils"
5
5
 
6
6
  module Twitter
7
7
  module REST
@@ -32,7 +32,7 @@ module Twitter
32
32
  def saved_searches(*args)
33
33
  arguments = Twitter::Arguments.new(args)
34
34
  if arguments.empty?
35
- perform_get_with_objects('/1.1/saved_searches/list.json', arguments.options, Twitter::SavedSearch)
35
+ perform_get_with_objects("/1.1/saved_searches/list.json", arguments.options, Twitter::SavedSearch)
36
36
  else
37
37
  pmap(arguments) do |id|
38
38
  saved_search(id, arguments.options)
@@ -63,7 +63,7 @@ module Twitter
63
63
  # @param query [String] The query of the search the user would like to save.
64
64
  # @param options [Hash] A customizable set of options.
65
65
  def create_saved_search(query, options = {})
66
- perform_post_with_object('/1.1/saved_searches/create.json', options.merge(query: query), Twitter::SavedSearch)
66
+ perform_post_with_object("/1.1/saved_searches/create.json", options.merge(query: query), Twitter::SavedSearch)
67
67
  end
68
68
 
69
69
  # Destroys saved searches for the authenticated user
@@ -1,5 +1,5 @@
1
- require 'twitter/rest/request'
2
- require 'twitter/search_results'
1
+ require "twitter/rest/request"
2
+ require "twitter/search_results"
3
3
 
4
4
  module Twitter
5
5
  module REST
@@ -14,7 +14,7 @@ module Twitter
14
14
  # @rate_limited Yes
15
15
  # @authentication Requires user context
16
16
  # @raise [Twitter::Error::Unauthorized] Error raised when supplied user credentials are not valid.
17
- # @param q [String] A search term.
17
+ # @param query [String] A search term.
18
18
  # @param options [Hash] A customizable set of options.
19
19
  # @option options [String] :geocode Returns tweets by users located within a given radius of the given latitude/longitude. The location is preferentially taking from the Geotagging API, but will fall back to their Twitter profile. The parameter value is specified by "latitude,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers). Note that you cannot use the near operator via the API to geocode arbitrary locations; however you can use this geocode parameter to search near geocodes directly.
20
20
  # @option options [String] :lang Restricts tweets to the given language, given by an ISO 639-1 code.
@@ -25,11 +25,12 @@ module Twitter
25
25
  # @option options [Integer] :since_id Returns results with an ID greater than (that is, more recent than) the specified ID. There are limits to the number of Tweets which can be accessed through the API. If the limit of Tweets has occured since the since_id, the since_id will be forced to the oldest ID available.
26
26
  # @option options [Integer] :max_id Returns results with an ID less than (that is, older than) or equal to the specified ID.
27
27
  # @option options [Boolean] :include_entities The entities node will be disincluded when set to false.
28
+ # @option options [String] :tweet_mode The entities node will truncate or not tweet text. Options are "compat" and "extended". The current default is "compat" (truncate).
28
29
  # @return [Twitter::SearchResults] Return tweets that match a specified query with search metadata
29
- def search(q, options = {})
30
+ def search(query, options = {})
30
31
  options = options.dup
31
32
  options[:count] ||= MAX_TWEETS_PER_REQUEST
32
- request = Twitter::REST::Request.new(self, :get, '/1.1/search/tweets.json', options.merge(q: q))
33
+ request = Twitter::REST::Request.new(self, :get, "/1.1/search/tweets.json", options.merge(q: query))
33
34
  Twitter::SearchResults.new(request)
34
35
  end
35
36
  end
@@ -1,5 +1,5 @@
1
- require 'twitter/rest/utils'
2
- require 'twitter/user'
1
+ require "twitter/rest/utils"
2
+ require "twitter/user"
3
3
 
4
4
  module Twitter
5
5
  module REST
@@ -19,7 +19,7 @@ module Twitter
19
19
  # @param users [Enumerable<Integer, String, Twitter::User>] A collection of Twitter user IDs, screen names, or objects.
20
20
  # @param options [Hash] A customizable set of options.
21
21
  def report_spam(*args)
22
- parallel_users_from_response(:post, '/1.1/users/report_spam.json', args)
22
+ parallel_users_from_response(:post, "/1.1/users/report_spam.json", args)
23
23
  end
24
24
  end
25
25
  end
@@ -1,7 +1,7 @@
1
- require 'twitter/arguments'
2
- require 'twitter/rest/utils'
3
- require 'twitter/suggestion'
4
- require 'twitter/user'
1
+ require "twitter/arguments"
2
+ require "twitter/rest/utils"
3
+ require "twitter/suggestion"
4
+ require "twitter/user"
5
5
 
6
6
  module Twitter
7
7
  module REST
@@ -28,7 +28,7 @@ module Twitter
28
28
  if arguments.last
29
29
  perform_get_with_object("/1.1/users/suggestions/#{arguments.pop}.json", arguments.options, Twitter::Suggestion)
30
30
  else
31
- perform_get_with_objects('/1.1/users/suggestions.json', arguments.options, Twitter::Suggestion)
31
+ perform_get_with_objects("/1.1/users/suggestions.json", arguments.options, Twitter::Suggestion)
32
32
  end
33
33
  end
34
34
 
@@ -1,6 +1,6 @@
1
- require 'twitter/rest/utils'
2
- require 'twitter/tweet'
3
- require 'twitter/user'
1
+ require "twitter/rest/utils"
2
+ require "twitter/tweet"
3
+ require "twitter/user"
4
4
 
5
5
  module Twitter
6
6
  module REST
@@ -23,7 +23,7 @@ module Twitter
23
23
  # @option options [Integer] :count Specifies the number of records to retrieve. Must be less than or equal to 200.
24
24
  # @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.
25
25
  def mentions_timeline(options = {})
26
- perform_get_with_objects('/1.1/statuses/mentions_timeline.json', options, Twitter::Tweet)
26
+ perform_get_with_objects("/1.1/statuses/mentions_timeline.json", options, Twitter::Tweet)
27
27
  end
28
28
  alias mentions mentions_timeline
29
29
 
@@ -46,7 +46,7 @@ module Twitter
46
46
  # @option options [Boolean, String, Integer] :contributor_details Specifies that the contributors element should be enhanced to include the screen_name of the contributor.
47
47
  # @option options [Boolean, String, Integer] :include_rts Specifies that the timeline should include native retweets in addition to regular tweets. Note: If you're using the trim_user parameter in conjunction with include_rts, the retweets will no longer contain a full user object.
48
48
  def user_timeline(*args)
49
- objects_from_response_with_user(Twitter::Tweet, :get, '/1.1/statuses/user_timeline.json', args)
49
+ objects_from_response_with_user(Twitter::Tweet, :get, "/1.1/statuses/user_timeline.json", args)
50
50
  end
51
51
 
52
52
  # Returns the 20 most recent retweets posted by the specified user
@@ -110,7 +110,7 @@ module Twitter
110
110
  # @option options [Boolean, String, Integer] :include_rts Specifies that the timeline should include native retweets in addition to regular tweets. Note: If you're using the trim_user parameter in conjunction with include_rts, the retweets will no longer contain a full user object.
111
111
  # @option options [Boolean, String, Integer] :contributor_details Specifies that the contributors element should be enhanced to include the screen_name of the contributor.
112
112
  def home_timeline(options = {})
113
- perform_get_with_objects('/1.1/statuses/home_timeline.json', options, Twitter::Tweet)
113
+ perform_get_with_objects("/1.1/statuses/home_timeline.json", options, Twitter::Tweet)
114
114
  end
115
115
 
116
116
  # Returns the 20 most recent retweets posted by users the authenticating user follow.
@@ -148,7 +148,7 @@ module Twitter
148
148
  # @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.
149
149
  # @option options [Boolean, String, Integer] :include_user_entities The user entities node will be disincluded when set to false.
150
150
  def retweets_of_me(options = {})
151
- perform_get_with_objects('/1.1/statuses/retweets_of_me.json', options, Twitter::Tweet)
151
+ perform_get_with_objects("/1.1/statuses/retweets_of_me.json", options, Twitter::Tweet)
152
152
  end
153
153
 
154
154
  private
@@ -188,6 +188,7 @@ module Twitter
188
188
  def collect_with_max_id(collection = [], max_id = nil, &block)
189
189
  tweets = yield(max_id)
190
190
  return collection if tweets.nil?
191
+
191
192
  collection += tweets
192
193
  tweets.empty? ? collection.flatten : collect_with_max_id(collection, tweets.last.id - 1, &block)
193
194
  end
@@ -1,7 +1,7 @@
1
- require 'twitter/place'
2
- require 'twitter/rest/request'
3
- require 'twitter/rest/utils'
4
- require 'twitter/trend_results'
1
+ require "twitter/place"
2
+ require "twitter/rest/request"
3
+ require "twitter/rest/utils"
4
+ require "twitter/trend_results"
5
5
 
6
6
  module Twitter
7
7
  module REST
@@ -21,7 +21,7 @@ module Twitter
21
21
  def trends(id = 1, options = {})
22
22
  options = options.dup
23
23
  options[:id] = id
24
- response = perform_get('/1.1/trends/place.json', options).first
24
+ response = perform_get("/1.1/trends/place.json", options).first
25
25
  Twitter::TrendResults.new(response)
26
26
  end
27
27
  alias local_trends trends
@@ -36,7 +36,7 @@ module Twitter
36
36
  # @param options [Hash] A customizable set of options.
37
37
  # @return [Array<Twitter::Place>]
38
38
  def trends_available(options = {})
39
- perform_get_with_objects('/1.1/trends/available.json', options, Twitter::Place)
39
+ perform_get_with_objects("/1.1/trends/available.json", options, Twitter::Place)
40
40
  end
41
41
  alias trend_locations trends_available
42
42
 
@@ -51,7 +51,7 @@ module Twitter
51
51
  # @option options [Float] :long If provided with a :lat option the available trend locations will be sorted by distance, nearest to furthest, to the co-ordinate pair. The valid ranges for longitude are -180.0 to +180.0 (East is positive) inclusive.
52
52
  # @return [Array<Twitter::Place>]
53
53
  def trends_closest(options = {})
54
- perform_get_with_objects('/1.1/trends/closest.json', options, Twitter::Place)
54
+ perform_get_with_objects("/1.1/trends/closest.json", options, Twitter::Place)
55
55
  end
56
56
  end
57
57
  end
@@ -1,14 +1,16 @@
1
- require 'twitter/arguments'
2
- require 'twitter/error'
3
- require 'twitter/oembed'
4
- require 'twitter/rest/request'
5
- require 'twitter/rest/utils'
6
- require 'twitter/tweet'
7
- require 'twitter/utils'
1
+ require "twitter/arguments"
2
+ require "twitter/error"
3
+ require "twitter/oembed"
4
+ require "twitter/rest/request"
5
+ require "twitter/rest/upload_utils"
6
+ require "twitter/rest/utils"
7
+ require "twitter/tweet"
8
+ require "twitter/utils"
8
9
 
9
10
  module Twitter
10
11
  module REST
11
12
  module Tweets
13
+ include Twitter::REST::UploadUtils
12
14
  include Twitter::REST::Utils
13
15
  include Twitter::Utils
14
16
  MAX_TWEETS_PER_REQUEST = 100
@@ -77,7 +79,7 @@ module Twitter
77
79
  def statuses(*args)
78
80
  arguments = Twitter::Arguments.new(args)
79
81
  flat_pmap(arguments.each_slice(MAX_TWEETS_PER_REQUEST)) do |tweets|
80
- perform_post_with_objects('/1.1/statuses/lookup.json', arguments.options.merge(id: tweets.collect { |u| extract_id(u) }.join(',')), Twitter::Tweet)
82
+ perform_post_with_objects("/1.1/statuses/lookup.json", arguments.options.merge(id: tweets.collect { |u| extract_id(u) }.join(",")), Twitter::Tweet)
81
83
  end
82
84
  end
83
85
 
@@ -152,7 +154,7 @@ module Twitter
152
154
  hash = options.dup
153
155
  hash[:in_reply_to_status_id] = hash.delete(:in_reply_to_status).id unless hash[:in_reply_to_status].nil?
154
156
  hash[:place_id] = hash.delete(:place).woeid unless hash[:place].nil?
155
- perform_post_with_object('/1.1/statuses/update.json', hash.merge(status: status), Twitter::Tweet)
157
+ perform_post_with_object("/1.1/statuses/update.json", hash.merge(status: status), Twitter::Tweet)
156
158
  end
157
159
 
158
160
  # Retweets the specified Tweets as the authenticating user
@@ -171,11 +173,9 @@ module Twitter
171
173
  def retweet(*args)
172
174
  arguments = Twitter::Arguments.new(args)
173
175
  pmap(arguments) do |tweet|
174
- begin
175
- post_retweet(extract_id(tweet), arguments.options)
176
- rescue Twitter::Error::AlreadyRetweeted, Twitter::Error::NotFound
177
- next
178
- end
176
+ post_retweet(extract_id(tweet), arguments.options)
177
+ rescue Twitter::Error::AlreadyRetweeted, Twitter::Error::NotFound
178
+ next
179
179
  end.compact
180
180
  end
181
181
 
@@ -226,7 +226,7 @@ module Twitter
226
226
  media_ids = pmap(array_wrap(media)) do |medium|
227
227
  upload(medium)[:media_id]
228
228
  end
229
- update!(status, options.merge(media_ids: media_ids.join(',')))
229
+ update!(status, options.merge(media_ids: media_ids.join(",")))
230
230
  end
231
231
 
232
232
  # Returns oEmbed for a Tweet
@@ -250,7 +250,7 @@ module Twitter
250
250
  def oembed(tweet, options = {})
251
251
  options = options.dup
252
252
  options[:id] = extract_id(tweet)
253
- perform_get_with_object('/1.1/statuses/oembed.json', options, Twitter::OEmbed)
253
+ perform_get_with_object("/1.1/statuses/oembed.json", options, Twitter::OEmbed)
254
254
  end
255
255
 
256
256
  # Returns oEmbeds for Tweets
@@ -294,7 +294,7 @@ module Twitter
294
294
  def retweeters_ids(*args)
295
295
  arguments = Twitter::Arguments.new(args)
296
296
  arguments.options[:id] ||= extract_id(arguments.first)
297
- perform_get_with_cursor('/1.1/statuses/retweeters/ids.json', arguments.options, :ids)
297
+ perform_get_with_cursor("/1.1/statuses/retweeters/ids.json", arguments.options, :ids)
298
298
  end
299
299
 
300
300
  # Untweets a retweeted status as the authenticating user
@@ -313,47 +313,14 @@ module Twitter
313
313
  def unretweet(*args)
314
314
  arguments = Twitter::Arguments.new(args)
315
315
  pmap(arguments) do |tweet|
316
- begin
317
- post_unretweet(extract_id(tweet), arguments.options)
318
- rescue Twitter::Error::NotFound
319
- next
320
- end
316
+ post_unretweet(extract_id(tweet), arguments.options)
317
+ rescue Twitter::Error::NotFound
318
+ next
321
319
  end.compact
322
320
  end
323
321
 
324
322
  private
325
323
 
326
- # Uploads images and videos. Videos require multiple requests and uploads in chunks of 5 Megabytes.
327
- # The only supported video format is mp4.
328
- #
329
- # @see https://dev.twitter.com/rest/public/uploading-media
330
- def upload(media) # rubocop:disable MethodLength, AbcSize
331
- if File.basename(media) !~ /\.mp4$/
332
- Twitter::REST::Request.new(self, :multipart_post, 'https://upload.twitter.com/1.1/media/upload.json', key: :media, file: media).perform
333
- else
334
- init = Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json',
335
- command: 'INIT',
336
- media_type: 'video/mp4',
337
- total_bytes: media.size).perform
338
-
339
- until media.eof?
340
- chunk = media.read(5_000_000)
341
- seg ||= -1
342
- Twitter::REST::Request.new(self, :multipart_post, 'https://upload.twitter.com/1.1/media/upload.json',
343
- command: 'APPEND',
344
- media_id: init[:media_id],
345
- segment_index: seg += 1,
346
- key: :media,
347
- file: StringIO.new(chunk)).perform
348
- end
349
-
350
- media.close
351
-
352
- Twitter::REST::Request.new(self, :post, 'https://upload.twitter.com/1.1/media/upload.json',
353
- command: 'FINALIZE', media_id: init[:media_id]).perform
354
- end
355
- end
356
-
357
324
  def array_wrap(object)
358
325
  if object.respond_to?(:to_ary)
359
326
  object.to_ary || [object]
@@ -1,8 +1,8 @@
1
- require 'twitter/arguments'
2
- require 'twitter/cursor'
3
- require 'twitter/rest/utils'
4
- require 'twitter/tweet'
5
- require 'twitter/user'
1
+ require "twitter/arguments"
2
+ require "twitter/cursor"
3
+ require "twitter/rest/utils"
4
+ require "twitter/tweet"
5
+ require "twitter/user"
6
6
 
7
7
  module Twitter
8
8
  module REST
@@ -24,7 +24,7 @@ module Twitter
24
24
  # @param user [Integer, String, Twitter::User] A Twitter user ID, screen name, URI, or object.
25
25
  # @param options [Hash] A customizable set of options.
26
26
  def following_followers_of(*args)
27
- cursor_from_response_with_user(:users, Twitter::User, '/users/following_followers_of.json', args)
27
+ cursor_from_response_with_user(:users, Twitter::User, "/users/following_followers_of.json", args)
28
28
  end
29
29
 
30
30
  # Returns Tweets count for a URI
@@ -36,7 +36,7 @@ module Twitter
36
36
  # @param url [String, URI] A URL.
37
37
  # @param options [Hash] A customizable set of options.
38
38
  def tweet_count(url, options = {})
39
- HTTP.get('https://cdn.api.twitter.com/1/urls/count.json', params: options.merge(url: url.to_s)).parse['count']
39
+ HTTP.get("https://cdn.api.twitter.com/1/urls/count.json", params: options.merge(url: url.to_s)).parse["count"]
40
40
  end
41
41
  end
42
42
  end
@@ -0,0 +1,68 @@
1
+ require "twitter/rest/request"
2
+
3
+ module Twitter
4
+ module REST
5
+ module UploadUtils
6
+ private
7
+
8
+ # Uploads images and videos. Videos require multiple requests and uploads in chunks of 5 Megabytes.
9
+ # The only supported video format is mp4.
10
+ #
11
+ # @see https://developer.twitter.com/en/docs/media/upload-media/uploading-media/media-best-practices
12
+ def upload(media, media_category_prefix: "tweet")
13
+ return chunk_upload(media, "video/mp4", "#{media_category_prefix}_video") if File.extname(media) == ".mp4"
14
+ return chunk_upload(media, "image/gif", "#{media_category_prefix}_gif") if File.extname(media) == ".gif" && File.size(media) > 5_000_000
15
+
16
+ Twitter::REST::Request.new(self, :multipart_post, "https://upload.twitter.com/1.1/media/upload.json", key: :media, file: media).perform
17
+ end
18
+
19
+ # @raise [Twitter::Error::TimeoutError] Error raised when the upload is longer than the value specified in Twitter::Client#timeouts[:upload].
20
+ # @raise [Twitter::Error::MediaError] Error raised when Twitter return an error about a media which is not mapped by the gem.
21
+ # @raise [Twitter::Error::MediaInternalError] Error raised when Twitter returns an InternalError error.
22
+ # @raise [Twitter::Error::InvalidMedia] Error raised when Twitter returns an InvalidMedia error.
23
+ # @raise [Twitter::Error::UnsupportedMedia] Error raised when Twitter returns an UnsupportedMedia error.
24
+ # @see https://developer.twitter.com/en/docs/media/upload-media/uploading-media/chunked-media-upload
25
+ def chunk_upload(media, media_type, media_category)
26
+ Timeout.timeout(timeouts&.fetch(:upload, nil), Twitter::Error::TimeoutError) do
27
+ init = Twitter::REST::Request.new(self, :post, "https://upload.twitter.com/1.1/media/upload.json",
28
+ command: "INIT",
29
+ media_type: media_type,
30
+ media_category: media_category,
31
+ total_bytes: media.size).perform
32
+ append_media(media, init[:media_id])
33
+ media.close
34
+ finalize_media(init[:media_id])
35
+ end
36
+ end
37
+
38
+ # @see https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-append
39
+ def append_media(media, media_id)
40
+ until media.eof?
41
+ chunk = media.read(5_000_000)
42
+ seg ||= -1
43
+ Twitter::REST::Request.new(self, :multipart_post, "https://upload.twitter.com/1.1/media/upload.json",
44
+ command: "APPEND",
45
+ media_id: media_id,
46
+ segment_index: seg += 1,
47
+ key: :media,
48
+ file: StringIO.new(chunk)).perform
49
+ end
50
+ end
51
+
52
+ # @see https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-upload-finalize
53
+ # @see https://developer.twitter.com/en/docs/media/upload-media/api-reference/get-media-upload-status
54
+ def finalize_media(media_id)
55
+ response = Twitter::REST::Request.new(self, :post, "https://upload.twitter.com/1.1/media/upload.json",
56
+ command: "FINALIZE", media_id: media_id).perform
57
+ loop do
58
+ return response if !response[:processing_info] || %w[failed succeeded].include?(response[:processing_info][:state])
59
+
60
+ sleep(response[:processing_info][:check_after_secs])
61
+ response = Twitter::REST::Request.new(self, :get, "https://upload.twitter.com/1.1/media/upload.json",
62
+ command: "STATUS", media_id: media_id).perform
63
+ end
64
+ response
65
+ end
66
+ end
67
+ end
68
+ end