simple_tweet 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|