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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -10
- data/LICENSE.md +1 -1
- data/README.md +9 -14
- data/lib/twitter/base.rb +11 -11
- data/lib/twitter/basic_user.rb +3 -2
- data/lib/twitter/client.rb +8 -8
- data/lib/twitter/creatable.rb +7 -3
- data/lib/twitter/cursor.rb +15 -5
- data/lib/twitter/direct_message.rb +4 -3
- data/lib/twitter/direct_message_event.rb +44 -0
- data/lib/twitter/direct_messages/welcome_message.rb +17 -0
- data/lib/twitter/direct_messages/welcome_message_rule.rb +12 -0
- data/lib/twitter/direct_messages/welcome_message_rule_wrapper.rb +36 -0
- data/lib/twitter/direct_messages/welcome_message_wrapper.rb +42 -0
- data/lib/twitter/entities.rb +6 -6
- data/lib/twitter/entity/hashtag.rb +1 -1
- data/lib/twitter/entity/symbol.rb +1 -1
- data/lib/twitter/entity/uri.rb +1 -1
- data/lib/twitter/entity/user_mention.rb +1 -1
- data/lib/twitter/entity.rb +1 -1
- data/lib/twitter/enumerable.rb +15 -6
- data/lib/twitter/error.rb +52 -9
- data/lib/twitter/factory.rb +1 -1
- data/lib/twitter/geo/point.rb +1 -1
- data/lib/twitter/geo/polygon.rb +1 -1
- data/lib/twitter/geo.rb +2 -2
- data/lib/twitter/geo_factory.rb +3 -3
- data/lib/twitter/geo_results.rb +2 -2
- data/lib/twitter/headers.rb +4 -4
- data/lib/twitter/identity.rb +2 -2
- data/lib/twitter/language.rb +1 -1
- data/lib/twitter/list.rb +3 -2
- data/lib/twitter/media/animated_gif.rb +1 -1
- data/lib/twitter/media/photo.rb +2 -2
- data/lib/twitter/media/video.rb +3 -3
- data/lib/twitter/media/video_info.rb +2 -2
- data/lib/twitter/media_factory.rb +4 -4
- data/lib/twitter/metadata.rb +1 -1
- data/lib/twitter/null_object.rb +19 -5
- data/lib/twitter/oembed.rb +2 -1
- data/lib/twitter/place.rb +3 -3
- data/lib/twitter/premium_search_results.rb +67 -0
- data/lib/twitter/profile.rb +6 -6
- data/lib/twitter/profile_banner.rb +2 -2
- data/lib/twitter/rate_limit.rb +6 -6
- data/lib/twitter/relationship.rb +2 -1
- data/lib/twitter/rest/account_activity.rb +99 -0
- data/lib/twitter/rest/api.rb +22 -16
- data/lib/twitter/rest/client.rb +4 -4
- data/lib/twitter/rest/direct_messages/welcome_messages.rb +90 -0
- data/lib/twitter/rest/direct_messages.rb +136 -47
- data/lib/twitter/rest/favorites.rb +17 -21
- data/lib/twitter/rest/form_encoder.rb +27 -0
- data/lib/twitter/rest/friends_and_followers.rb +21 -21
- data/lib/twitter/rest/help.rb +6 -18
- data/lib/twitter/rest/lists.rb +33 -32
- data/lib/twitter/rest/oauth.rb +8 -8
- data/lib/twitter/rest/places_and_geo.rb +6 -6
- data/lib/twitter/rest/premium_search.rb +34 -0
- data/lib/twitter/rest/request.rb +64 -31
- data/lib/twitter/rest/saved_searches.rb +6 -6
- data/lib/twitter/rest/search.rb +6 -5
- data/lib/twitter/rest/spam_reporting.rb +3 -3
- data/lib/twitter/rest/suggested_users.rb +5 -5
- data/lib/twitter/rest/timelines.rb +8 -7
- data/lib/twitter/rest/trends.rb +7 -7
- data/lib/twitter/rest/tweets.rb +20 -53
- data/lib/twitter/rest/undocumented.rb +7 -7
- data/lib/twitter/rest/upload_utils.rb +68 -0
- data/lib/twitter/rest/users.rb +32 -32
- data/lib/twitter/rest/utils.rb +41 -24
- data/lib/twitter/saved_search.rb +2 -2
- data/lib/twitter/search_results.rb +10 -9
- data/lib/twitter/settings.rb +2 -1
- data/lib/twitter/size.rb +2 -2
- data/lib/twitter/source_user.rb +1 -1
- data/lib/twitter/streaming/client.rb +19 -15
- data/lib/twitter/streaming/connection.rb +19 -7
- data/lib/twitter/streaming/message_parser.rb +7 -7
- data/lib/twitter/streaming/response.rb +12 -10
- data/lib/twitter/suggestion.rb +3 -3
- data/lib/twitter/target_user.rb +1 -1
- data/lib/twitter/trend.rb +3 -2
- data/lib/twitter/trend_results.rb +5 -5
- data/lib/twitter/tweet.rb +13 -4
- data/lib/twitter/user.rb +9 -9
- data/lib/twitter/utils.rb +6 -4
- data/lib/twitter/variant.rb +2 -1
- data/lib/twitter/version.rb +4 -4
- data/lib/twitter.rb +36 -31
- data/twitter.gemspec +19 -19
- metadata +33 -37
- data/lib/twitter/configuration.rb +0 -27
data/lib/twitter/rest/request.rb
CHANGED
@@ -1,18 +1,19 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
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 =
|
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?(
|
28
|
-
|
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
|
-
|
36
|
-
|
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,
|
69
|
+
HTTP::FormData::File.new(file, content_type: "video/mp4")
|
50
70
|
else
|
51
|
-
HTTP::FormData::File.new(file, filename: 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
|
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
|
-
|
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
|
88
|
+
def content_type(basename)
|
67
89
|
case basename
|
68
90
|
when /\.gif$/i
|
69
|
-
|
91
|
+
"image/gif"
|
70
92
|
when /\.jpe?g/i
|
71
|
-
|
93
|
+
"image/jpeg"
|
72
94
|
when /\.png$/i
|
73
|
-
|
95
|
+
"image/png"
|
74
96
|
else
|
75
|
-
|
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
|
-
|
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
|
-
|
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(
|
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
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
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(
|
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(
|
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
|
data/lib/twitter/rest/search.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require
|
2
|
-
require
|
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
|
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(
|
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,
|
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
|
2
|
-
require
|
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,
|
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
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
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(
|
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
|
2
|
-
require
|
3
|
-
require
|
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(
|
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,
|
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(
|
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(
|
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
|
data/lib/twitter/rest/trends.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
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(
|
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(
|
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(
|
54
|
+
perform_get_with_objects("/1.1/trends/closest.json", options, Twitter::Place)
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
data/lib/twitter/rest/tweets.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
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(
|
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(
|
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
|
-
|
175
|
-
|
176
|
-
|
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(
|
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(
|
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
|
-
|
317
|
-
|
318
|
-
|
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
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
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,
|
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(
|
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
|