simple_tweet 2.2.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -2
- data/Gemfile +1 -3
- data/Gemfile.lock +77 -0
- data/lib/simple_tweet/v2_client.rb +163 -3
- data/lib/simple_tweet/version.rb +1 -1
- data/lib/simple_tweet.rb +6 -120
- metadata +3 -3
- data/lib/simple_tweet/v1_client.rb +0 -156
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cb994313a55f3a80782b1c73c983a7551fa2a394f49aa5149efdf59372d20d40
|
4
|
+
data.tar.gz: 2dff9773d979e4f907b1ad37072adc59d30d13c64d73ed26a004032a50cc2456
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5425884322fcb27da54bf4747c709ad412c9479daf0fd0b040209dbbec8be6f5d930505b4a72c85dabbef8a55b5817b5f5d83f7be2a8255d4e2aaa9253ec4e3
|
7
|
+
data.tar.gz: 31fb1439304050a6038b19ce25520795eb3588ebcdfe29b75b1249adbbdfff932f79833c131ef3492ff784599f2e4e3301222f7885784d60de2a634652d6cbef
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
simple_tweet (3.0.0)
|
5
|
+
multipart-post (>= 2.2.3)
|
6
|
+
oauth (~> 1.1.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
ast (2.4.2)
|
12
|
+
base64 (0.1.1)
|
13
|
+
diff-lcs (1.5.0)
|
14
|
+
hashie (5.0.0)
|
15
|
+
json (2.6.3)
|
16
|
+
language_server-protocol (3.17.0.3)
|
17
|
+
multipart-post (2.3.0)
|
18
|
+
oauth (1.1.0)
|
19
|
+
oauth-tty (~> 1.0, >= 1.0.1)
|
20
|
+
snaky_hash (~> 2.0)
|
21
|
+
version_gem (~> 1.1)
|
22
|
+
oauth-tty (1.0.5)
|
23
|
+
version_gem (~> 1.1, >= 1.1.1)
|
24
|
+
parallel (1.23.0)
|
25
|
+
parser (3.2.2.3)
|
26
|
+
ast (~> 2.4.1)
|
27
|
+
racc
|
28
|
+
racc (1.7.1)
|
29
|
+
rainbow (3.1.1)
|
30
|
+
rake (13.0.6)
|
31
|
+
regexp_parser (2.8.1)
|
32
|
+
rexml (3.2.6)
|
33
|
+
rspec (3.12.0)
|
34
|
+
rspec-core (~> 3.12.0)
|
35
|
+
rspec-expectations (~> 3.12.0)
|
36
|
+
rspec-mocks (~> 3.12.0)
|
37
|
+
rspec-core (3.12.1)
|
38
|
+
rspec-support (~> 3.12.0)
|
39
|
+
rspec-expectations (3.12.2)
|
40
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
41
|
+
rspec-support (~> 3.12.0)
|
42
|
+
rspec-mocks (3.12.3)
|
43
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
44
|
+
rspec-support (~> 3.12.0)
|
45
|
+
rspec-support (3.12.0)
|
46
|
+
rubocop (1.56.2)
|
47
|
+
base64 (~> 0.1.1)
|
48
|
+
json (~> 2.3)
|
49
|
+
language_server-protocol (>= 3.17.0)
|
50
|
+
parallel (~> 1.10)
|
51
|
+
parser (>= 3.2.2.3)
|
52
|
+
rainbow (>= 2.2.2, < 4.0)
|
53
|
+
regexp_parser (>= 1.8, < 3.0)
|
54
|
+
rexml (>= 3.2.5, < 4.0)
|
55
|
+
rubocop-ast (>= 1.28.1, < 2.0)
|
56
|
+
ruby-progressbar (~> 1.7)
|
57
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
58
|
+
rubocop-ast (1.29.0)
|
59
|
+
parser (>= 3.2.1.0)
|
60
|
+
ruby-progressbar (1.13.0)
|
61
|
+
snaky_hash (2.0.1)
|
62
|
+
hashie
|
63
|
+
version_gem (~> 1.1, >= 1.1.1)
|
64
|
+
unicode-display_width (2.4.2)
|
65
|
+
version_gem (1.1.3)
|
66
|
+
|
67
|
+
PLATFORMS
|
68
|
+
arm64-darwin-21
|
69
|
+
|
70
|
+
DEPENDENCIES
|
71
|
+
rake (~> 13.0)
|
72
|
+
rspec (~> 3.0)
|
73
|
+
rubocop (~> 1.56)
|
74
|
+
simple_tweet!
|
75
|
+
|
76
|
+
BUNDLED WITH
|
77
|
+
2.3.24
|
@@ -1,18 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "json"
|
4
|
+
require "cgi"
|
5
|
+
require "oauth"
|
6
|
+
require "net/http/post/multipart"
|
2
7
|
|
3
8
|
module SimpleTweet
|
4
9
|
module V2
|
5
|
-
#
|
6
|
-
class Client
|
10
|
+
# Twitte API v2を叩くクライアント
|
11
|
+
class Client
|
12
|
+
TW_API_ORIGIN = "https://api.twitter.com"
|
13
|
+
TW_UPLOAD_ORIGIN = "https://upload.twitter.com"
|
14
|
+
TW_MEDIA_UPLOAD_PATH = "/1.1/media/upload.json"
|
15
|
+
TW_METADATA_CREATE_PATH = "/1.1/media/metadata/create.json"
|
7
16
|
TW_TWEET_PATH = "/2/tweets"
|
8
|
-
UA = "SimpleTweet/#{SimpleTweet::VERSION}"
|
17
|
+
UA = "SimpleTweet/#{SimpleTweet::VERSION}".freeze
|
18
|
+
APPEND_PER = 5 * (1 << 20)
|
19
|
+
|
20
|
+
def initialize(consumer_key:, consumer_secret:, access_token:, access_token_secret:, max_append_retry: 3)
|
21
|
+
@consumer_key_ = consumer_key
|
22
|
+
@consumer_secret_ = consumer_secret
|
23
|
+
@access_token_ = access_token
|
24
|
+
@access_token_secret_ = access_token_secret
|
25
|
+
@max_append_retry_ = max_append_retry
|
26
|
+
end
|
9
27
|
|
28
|
+
# https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/migrate
|
10
29
|
def tweet(message:, media_ids: [])
|
11
30
|
json = { text: message }
|
12
31
|
json[:media] = { media_ids: media_ids } unless media_ids.empty?
|
13
32
|
header = { "User-Agent": UA, "content-type": "application/json" }
|
14
33
|
access_token.post(TW_TWEET_PATH, json.to_json, header)
|
15
34
|
end
|
35
|
+
|
36
|
+
def tweet_with_media(message:, media_type:, media:, alt_text: nil)
|
37
|
+
media_ids = upload_media(media_type: media_type, media: media)
|
38
|
+
unless alt_text.nil?
|
39
|
+
media_ids.each do |media_id|
|
40
|
+
create_media_metadata(media_id: media_id, alt_text: alt_text)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
tweet(message: message, media_ids: media_ids)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def access_token(site: TW_API_ORIGIN)
|
49
|
+
consumer = ::OAuth::Consumer.new(@consumer_key_, @consumer_secret_, site: site)
|
50
|
+
::OAuth::AccessToken.new(consumer, @access_token_, @access_token_secret_)
|
51
|
+
end
|
52
|
+
|
53
|
+
def request(req)
|
54
|
+
@client ||= access_token(site: TW_UPLOAD_ORIGIN)
|
55
|
+
@client.sign! req
|
56
|
+
|
57
|
+
url = ::URI.parse(TW_UPLOAD_ORIGIN + TW_MEDIA_UPLOAD_PATH)
|
58
|
+
https = ::Net::HTTP.new(url.host, url.port)
|
59
|
+
https.use_ssl = true
|
60
|
+
|
61
|
+
https.start do |http|
|
62
|
+
http.request req
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload
|
67
|
+
## maybe todo: multiple image
|
68
|
+
# ここはv1のAPIを叩いている。
|
69
|
+
def upload_media(media_type:, media:)
|
70
|
+
return upload_video(video: media) if media_type == "video/mp4"
|
71
|
+
|
72
|
+
req = ::Net::HTTP::Post::Multipart.new(
|
73
|
+
TW_MEDIA_UPLOAD_PATH,
|
74
|
+
media: ::UploadIO.new(media, media_type),
|
75
|
+
media_category: "tweet_image"
|
76
|
+
)
|
77
|
+
res = ::JSON.parse(request(req).body)
|
78
|
+
[res["media_id_string"]]
|
79
|
+
end
|
80
|
+
|
81
|
+
def init(video:)
|
82
|
+
init_req = ::Net::HTTP::Post::Multipart.new(
|
83
|
+
TW_MEDIA_UPLOAD_PATH,
|
84
|
+
command: "INIT",
|
85
|
+
total_bytes: video.size,
|
86
|
+
media_type: "video/mp4"
|
87
|
+
)
|
88
|
+
init_res = request(init_req)
|
89
|
+
raise UploadMediaError.new("init failed", response: init_res) unless init_res.code == "202"
|
90
|
+
|
91
|
+
::JSON.parse(init_res.body)
|
92
|
+
end
|
93
|
+
|
94
|
+
def append(video:, media_id:, index:, retry_count: 0)
|
95
|
+
append_req = ::Net::HTTP::Post::Multipart.new(
|
96
|
+
TW_MEDIA_UPLOAD_PATH,
|
97
|
+
command: "APPEND",
|
98
|
+
media_id: media_id,
|
99
|
+
media: video.read(APPEND_PER),
|
100
|
+
segment_index: index
|
101
|
+
)
|
102
|
+
res = request(append_req)
|
103
|
+
return if res.code == "204"
|
104
|
+
raise UploadMediaError.new("append failed", response: res) unless retry_count <= @max_append_retry_
|
105
|
+
|
106
|
+
append(video: video, media_id: media_id, index: index, retry_count: retry_count + 1)
|
107
|
+
end
|
108
|
+
|
109
|
+
def finalize(media_id:)
|
110
|
+
finalize_req = ::Net::HTTP::Post::Multipart.new(
|
111
|
+
TW_MEDIA_UPLOAD_PATH,
|
112
|
+
command: "FINALIZE",
|
113
|
+
media_id: media_id
|
114
|
+
)
|
115
|
+
finalize_res = request(finalize_req)
|
116
|
+
raise UploadMediaError.new("finalize failed", response: finalize_res) unless finalize_res.code == "201"
|
117
|
+
|
118
|
+
::JSON.parse(finalize_res.body)
|
119
|
+
end
|
120
|
+
|
121
|
+
def status(media_id:)
|
122
|
+
status_req = ::Net::HTTP::Post::Multipart.new(
|
123
|
+
TW_MEDIA_UPLOAD_PATH,
|
124
|
+
command: "STATUS",
|
125
|
+
media_id: media_id
|
126
|
+
)
|
127
|
+
status_res = request(status_req)
|
128
|
+
raise UploadMediaError.new("status failed", response: status_res) unless status_res.code == "200"
|
129
|
+
|
130
|
+
::JSON.parse(status_res.body)
|
131
|
+
end
|
132
|
+
|
133
|
+
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload-init
|
134
|
+
def upload_video(video:)
|
135
|
+
init_res = init(video: video)
|
136
|
+
media_id = init_res["media_id_string"]
|
137
|
+
|
138
|
+
chunks_needed = (video.size - 1) / APPEND_PER + 1
|
139
|
+
chunks_needed.times do |i|
|
140
|
+
append(video: video, media_id: media_id, index: i)
|
141
|
+
end
|
142
|
+
|
143
|
+
finalize_res = finalize(media_id: media_id)
|
144
|
+
|
145
|
+
if finalize_res["processing_info"]
|
146
|
+
retry_after = finalize_res["processing_info"]["check_after_secs"] || 5
|
147
|
+
loop do
|
148
|
+
sleep retry_after
|
149
|
+
|
150
|
+
status_res = status(media_id: media_id)
|
151
|
+
raise UploadMediaError if status_res["processing_info"].nil?
|
152
|
+
break if status_res["processing_info"]["state"] == "succeeded"
|
153
|
+
|
154
|
+
if status_res["processing_info"]["state"] == "in_progress"
|
155
|
+
retry_after = status_res["processing_info"]["check_after_secs"] || 5
|
156
|
+
next
|
157
|
+
end
|
158
|
+
|
159
|
+
# status_res_json["processing_info"]["state"] == "failed"
|
160
|
+
raise UploadMediaError
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
[media_id]
|
165
|
+
end
|
166
|
+
|
167
|
+
def create_media_metadata(media_id:, alt_text:)
|
168
|
+
header = { "content-type": "application/json; charset=UTF-8" }
|
169
|
+
req = ::Net::HTTP::Post.new(TW_METADATA_CREATE_PATH, header)
|
170
|
+
req.body = { media_id: media_id, alt_text: { text: alt_text } }.to_json
|
171
|
+
res = request(req)
|
172
|
+
raise UploadMediaError.new("create_media_metadata failed", response: res) if res.code != "200"
|
173
|
+
|
174
|
+
res
|
175
|
+
end
|
16
176
|
end
|
17
177
|
end
|
18
178
|
end
|
data/lib/simple_tweet/version.rb
CHANGED
data/lib/simple_tweet.rb
CHANGED
@@ -1,132 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "simple_tweet/version"
|
4
|
-
require_relative "simple_tweet/v1_client"
|
5
4
|
require_relative "simple_tweet/v2_client"
|
6
|
-
require "json"
|
7
5
|
|
8
6
|
module SimpleTweet
|
9
7
|
class Error < ::StandardError; end
|
10
|
-
class UploadMediaError < Error; end
|
11
8
|
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
## 逆に、本体とuserのところだけHashから変換している。
|
16
|
-
Tweet = ::Struct.new(
|
17
|
-
:created_at,
|
18
|
-
:id,
|
19
|
-
:id_str,
|
20
|
-
:text,
|
21
|
-
:truncated,
|
22
|
-
:entities,
|
23
|
-
:source,
|
24
|
-
:in_reply_to_status_id,
|
25
|
-
:in_reply_to_status_id_str,
|
26
|
-
:in_reply_to_user_id,
|
27
|
-
:in_reply_to_user_id_str,
|
28
|
-
:in_reply_to_screen_name,
|
29
|
-
:user,
|
30
|
-
:geo,
|
31
|
-
:coordinates,
|
32
|
-
:place,
|
33
|
-
:contributors,
|
34
|
-
:is_quote_status,
|
35
|
-
:retweet_count,
|
36
|
-
:favorite_count,
|
37
|
-
:favorited,
|
38
|
-
:retweeted,
|
39
|
-
:lang,
|
40
|
-
:extended_entities,
|
41
|
-
:possibly_sensitive,
|
42
|
-
:quoted_status_id,
|
43
|
-
:quoted_status_id_str,
|
44
|
-
:quoted_status,
|
45
|
-
keyword_init: true
|
46
|
-
)
|
47
|
-
User = ::Struct.new(
|
48
|
-
:id,
|
49
|
-
:id_str,
|
50
|
-
:name,
|
51
|
-
:screen_name,
|
52
|
-
:location,
|
53
|
-
:description,
|
54
|
-
:url,
|
55
|
-
:entities,
|
56
|
-
:protected,
|
57
|
-
:followers_count,
|
58
|
-
:friends_count,
|
59
|
-
:listed_count,
|
60
|
-
:created_at,
|
61
|
-
:favourites_count,
|
62
|
-
:utc_offset,
|
63
|
-
:time_zone,
|
64
|
-
:geo_enabled,
|
65
|
-
:verified,
|
66
|
-
:statuses_count,
|
67
|
-
:lang,
|
68
|
-
:contributors_enabled,
|
69
|
-
:is_translator,
|
70
|
-
:is_translation_enabled,
|
71
|
-
:profile_background_color,
|
72
|
-
:profile_background_image_url,
|
73
|
-
:profile_background_image_url_https,
|
74
|
-
:profile_background_tile,
|
75
|
-
:profile_image_url,
|
76
|
-
:profile_image_url_https,
|
77
|
-
:profile_banner_url,
|
78
|
-
:profile_link_color,
|
79
|
-
:profile_sidebar_border_color,
|
80
|
-
:profile_sidebar_fill_color,
|
81
|
-
:profile_text_color,
|
82
|
-
:profile_use_background_image,
|
83
|
-
:has_extended_profile,
|
84
|
-
:default_profile,
|
85
|
-
:default_profile_image,
|
86
|
-
:following,
|
87
|
-
:follow_request_sent,
|
88
|
-
:notifications,
|
89
|
-
:translator_type,
|
90
|
-
:withheld_in_countries,
|
91
|
-
keyword_init: true
|
92
|
-
)
|
9
|
+
# UploadMediaError is
|
10
|
+
class UploadMediaError < Error
|
11
|
+
attr_reader :response
|
93
12
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
return nil unless response.code == "200"
|
98
|
-
|
99
|
-
res = ::JSON.parse(response.body)
|
100
|
-
tw = Tweet.new(**res)
|
101
|
-
tw.created_at = ::Time.parse(tw.created_at).utc
|
102
|
-
tw.user = User.new(**res["user"])
|
103
|
-
tw
|
104
|
-
end
|
105
|
-
|
106
|
-
def uri
|
107
|
-
"https://twitter.com/#{user.screen_name}/status/#{id}"
|
108
|
-
end
|
109
|
-
alias url uri
|
110
|
-
|
111
|
-
def to_h
|
112
|
-
super.map do |k, v|
|
113
|
-
if k == :user
|
114
|
-
[k.to_s, v.to_h]
|
115
|
-
else
|
116
|
-
[k.to_s, v]
|
117
|
-
end
|
118
|
-
end.to_h
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# User is
|
123
|
-
class User
|
124
|
-
def protected?
|
125
|
-
protected
|
126
|
-
end
|
127
|
-
|
128
|
-
def to_h
|
129
|
-
super.transform_keys(&:to_s)
|
13
|
+
def initialize(message = nil, response: nil)
|
14
|
+
super(message)
|
15
|
+
@response = response
|
130
16
|
end
|
131
17
|
end
|
132
18
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple_tweet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kugayama Nana
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-09-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: multipart-post
|
@@ -49,11 +49,11 @@ files:
|
|
49
49
|
- ".rubocop.yml"
|
50
50
|
- CODE_OF_CONDUCT.md
|
51
51
|
- Gemfile
|
52
|
+
- Gemfile.lock
|
52
53
|
- LICENSE.txt
|
53
54
|
- README.md
|
54
55
|
- Rakefile
|
55
56
|
- lib/simple_tweet.rb
|
56
|
-
- lib/simple_tweet/v1_client.rb
|
57
57
|
- lib/simple_tweet/v2_client.rb
|
58
58
|
- lib/simple_tweet/version.rb
|
59
59
|
- sig/simple_tweet.rbs
|
@@ -1,156 +0,0 @@
|
|
1
|
-
require "cgi"
|
2
|
-
require "oauth"
|
3
|
-
require "json"
|
4
|
-
require "net/http/post/multipart"
|
5
|
-
|
6
|
-
module SimpleTweet
|
7
|
-
module V1
|
8
|
-
# Client provides only tweet
|
9
|
-
class Client
|
10
|
-
TW_API_ORIGIN = "https://api.twitter.com"
|
11
|
-
TW_UPLOAD_ORIGIN = "https://upload.twitter.com"
|
12
|
-
TW_MEDIA_UPLOAD_PATH = "/1.1/media/upload.json"
|
13
|
-
APPEND_PER = 5 * (1 << 20)
|
14
|
-
|
15
|
-
def initialize(consumer_key:, consumer_secret:, access_token:, access_token_secret:, max_append_retry: 3)
|
16
|
-
@consumer_key_ = consumer_key
|
17
|
-
@consumer_secret_ = consumer_secret
|
18
|
-
@access_token_ = access_token
|
19
|
-
@access_token_secret_ = access_token_secret
|
20
|
-
@max_append_retry_ = max_append_retry
|
21
|
-
end
|
22
|
-
|
23
|
-
# https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-update
|
24
|
-
def tweet(message:, media_ids: [])
|
25
|
-
path = "/1.1/statuses/update.json?status=#{::CGI.escape(message)}"
|
26
|
-
path += "&media_ids=#{media_ids.join(",")}" unless media_ids.empty?
|
27
|
-
Tweet.from_response(access_token.post(path))
|
28
|
-
end
|
29
|
-
|
30
|
-
# media_type is mime_type
|
31
|
-
def tweet_with_media(message:, media_type:, media:)
|
32
|
-
media_ids = upload_media(media_type: media_type, media: media)
|
33
|
-
tweet(message: message, media_ids: media_ids)
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def access_token(site: TW_API_ORIGIN)
|
39
|
-
consumer = ::OAuth::Consumer.new(@consumer_key_, @consumer_secret_, site: site)
|
40
|
-
::OAuth::AccessToken.new(consumer, @access_token_, @access_token_secret_)
|
41
|
-
end
|
42
|
-
|
43
|
-
def request(req)
|
44
|
-
@client ||= access_token(site: TW_UPLOAD_ORIGIN)
|
45
|
-
@client.sign! req
|
46
|
-
|
47
|
-
url = ::URI.parse(TW_UPLOAD_ORIGIN + TW_MEDIA_UPLOAD_PATH)
|
48
|
-
https = ::Net::HTTP.new(url.host, url.port)
|
49
|
-
https.use_ssl = true
|
50
|
-
|
51
|
-
https.start do |http|
|
52
|
-
http.request req
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload
|
57
|
-
## maybe todo: multiple image
|
58
|
-
def upload_media(media_type:, media:)
|
59
|
-
return upload_video(video: media) if media_type == "video/mp4"
|
60
|
-
|
61
|
-
req = ::Net::HTTP::Post::Multipart.new(
|
62
|
-
TW_MEDIA_UPLOAD_PATH,
|
63
|
-
media: ::UploadIO.new(media, media_type),
|
64
|
-
media_category: "tweet_image"
|
65
|
-
)
|
66
|
-
res = ::JSON.parse(request(req).body)
|
67
|
-
[res["media_id_string"]]
|
68
|
-
end
|
69
|
-
|
70
|
-
def init(video:)
|
71
|
-
init_req = ::Net::HTTP::Post::Multipart.new(
|
72
|
-
TW_MEDIA_UPLOAD_PATH,
|
73
|
-
command: "INIT",
|
74
|
-
total_bytes: video.size,
|
75
|
-
media_type: "video/mp4"
|
76
|
-
)
|
77
|
-
init_res = request(init_req)
|
78
|
-
raise UploadMediaError unless init_res.code == "202"
|
79
|
-
|
80
|
-
::JSON.parse(init_res.body)
|
81
|
-
end
|
82
|
-
|
83
|
-
def append(video:, media_id:, index:, retry_count: 0)
|
84
|
-
append_req = ::Net::HTTP::Post::Multipart.new(
|
85
|
-
TW_MEDIA_UPLOAD_PATH,
|
86
|
-
command: "APPEND",
|
87
|
-
media_id: media_id,
|
88
|
-
media: video.read(APPEND_PER),
|
89
|
-
segment_index: index
|
90
|
-
)
|
91
|
-
return if request(append_req).code == "204"
|
92
|
-
raise UploadMediaError unless retry_count <= @max_append_retry_
|
93
|
-
|
94
|
-
append(video: video, media_id: media_id, index: index, retry_count: retry_count + 1)
|
95
|
-
end
|
96
|
-
|
97
|
-
def finalize(media_id:)
|
98
|
-
finalize_req = ::Net::HTTP::Post::Multipart.new(
|
99
|
-
TW_MEDIA_UPLOAD_PATH,
|
100
|
-
command: "FINALIZE",
|
101
|
-
media_id: media_id
|
102
|
-
)
|
103
|
-
finalize_res = request(finalize_req)
|
104
|
-
raise UploadMediaError unless finalize_res.code == "201"
|
105
|
-
|
106
|
-
::JSON.parse(finalize_res.body)
|
107
|
-
end
|
108
|
-
|
109
|
-
def status(media_id:)
|
110
|
-
status_req = ::Net::HTTP::Post::Multipart.new(
|
111
|
-
TW_MEDIA_UPLOAD_PATH,
|
112
|
-
command: "STATUS",
|
113
|
-
media_id: media_id
|
114
|
-
)
|
115
|
-
status_res = request(status_req)
|
116
|
-
raise UploadMediaError unless status_res.code == "200"
|
117
|
-
|
118
|
-
::JSON.parse(status_res.body)
|
119
|
-
end
|
120
|
-
|
121
|
-
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload-init
|
122
|
-
def upload_video(video:)
|
123
|
-
init_res = init(video: video)
|
124
|
-
media_id = init_res["media_id_string"]
|
125
|
-
|
126
|
-
chunks_needed = (video.size - 1) / APPEND_PER + 1
|
127
|
-
chunks_needed.times do |i|
|
128
|
-
append(video: video, media_id: media_id, index: i)
|
129
|
-
end
|
130
|
-
|
131
|
-
finalize_res = finalize(media_id: media_id)
|
132
|
-
|
133
|
-
if finalize_res["processing_info"]
|
134
|
-
retry_after = finalize_res["processing_info"]["check_after_secs"] || 5
|
135
|
-
loop do
|
136
|
-
sleep retry_after
|
137
|
-
|
138
|
-
status_res = status(media_id: media_id)
|
139
|
-
raise UploadMediaError if status_res["processing_info"].nil?
|
140
|
-
break if status_res["processing_info"]["state"] == "succeeded"
|
141
|
-
|
142
|
-
if status_res["processing_info"]["state"] == "in_progress"
|
143
|
-
retry_after = status_res["processing_info"]["check_after_secs"] || 5
|
144
|
-
next
|
145
|
-
end
|
146
|
-
|
147
|
-
# status_res_json["processing_info"]["state"] == "failed"
|
148
|
-
raise UploadMediaError
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
[media_id]
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|