simple_tweet 2.3.0 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/Gemfile +2 -3
- data/Gemfile.lock +120 -0
- data/Steepfile +11 -0
- data/lib/simple_tweet/v2_client.rb +145 -6
- data/lib/simple_tweet/version.rb +1 -1
- data/lib/simple_tweet.rb +6 -120
- data/rbs_collection.lock.yaml +50 -0
- data/rbs_collection.yaml +17 -0
- data/sig/simple_tweet.rbs +42 -1
- metadata +6 -3
- data/lib/simple_tweet/v1_client.rb +0 -158
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a6ce5d4a7a448770b21c18cc4b68f25adc7f21d8814a02970864efc4c487d071
|
4
|
+
data.tar.gz: 6dfec2f42eff6f04cd8e0dc59571b31bc558b027fc20efdc87c59dd8402a47a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz: '
|
6
|
+
metadata.gz: bef5b277879212fc6c8a6c542ed70d71fd35e5c8e12ab0ab9119d50c3969e271b99b95f812b990eb4bb5ef93c9bc6b9726cb6e71fcf974112789e20da94e0b6f
|
7
|
+
data.tar.gz: '09771d8edada2fba52358e1f897410a620703d91fe87a6f1b602ec846c56e9771b4a3289908d4bcded74da60c396a235d074ae3388e61917da067422852346f5'
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
simple_tweet (3.0.1)
|
5
|
+
multipart-post (>= 2.2.3)
|
6
|
+
oauth (~> 1.1.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
activesupport (7.0.8)
|
12
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
|
+
i18n (>= 1.6, < 2)
|
14
|
+
minitest (>= 5.1)
|
15
|
+
tzinfo (~> 2.0)
|
16
|
+
ast (2.4.2)
|
17
|
+
base64 (0.1.1)
|
18
|
+
concurrent-ruby (1.2.2)
|
19
|
+
csv (3.2.7)
|
20
|
+
diff-lcs (1.5.0)
|
21
|
+
ffi (1.15.5)
|
22
|
+
fileutils (1.7.1)
|
23
|
+
hashie (5.0.0)
|
24
|
+
i18n (1.14.1)
|
25
|
+
concurrent-ruby (~> 1.0)
|
26
|
+
json (2.6.3)
|
27
|
+
language_server-protocol (3.17.0.3)
|
28
|
+
listen (3.8.0)
|
29
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
30
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
31
|
+
logger (1.5.3)
|
32
|
+
minitest (5.20.0)
|
33
|
+
multipart-post (2.3.0)
|
34
|
+
oauth (1.1.0)
|
35
|
+
oauth-tty (~> 1.0, >= 1.0.1)
|
36
|
+
snaky_hash (~> 2.0)
|
37
|
+
version_gem (~> 1.1)
|
38
|
+
oauth-tty (1.0.5)
|
39
|
+
version_gem (~> 1.1, >= 1.1.1)
|
40
|
+
parallel (1.23.0)
|
41
|
+
parser (3.2.2.3)
|
42
|
+
ast (~> 2.4.1)
|
43
|
+
racc
|
44
|
+
racc (1.7.1)
|
45
|
+
rainbow (3.1.1)
|
46
|
+
rake (13.0.6)
|
47
|
+
rb-fsevent (0.11.2)
|
48
|
+
rb-inotify (0.10.1)
|
49
|
+
ffi (~> 1.0)
|
50
|
+
rbs (3.2.1)
|
51
|
+
regexp_parser (2.8.1)
|
52
|
+
rexml (3.2.6)
|
53
|
+
rspec (3.12.0)
|
54
|
+
rspec-core (~> 3.12.0)
|
55
|
+
rspec-expectations (~> 3.12.0)
|
56
|
+
rspec-mocks (~> 3.12.0)
|
57
|
+
rspec-core (3.12.1)
|
58
|
+
rspec-support (~> 3.12.0)
|
59
|
+
rspec-expectations (3.12.2)
|
60
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
61
|
+
rspec-support (~> 3.12.0)
|
62
|
+
rspec-mocks (3.12.3)
|
63
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
64
|
+
rspec-support (~> 3.12.0)
|
65
|
+
rspec-support (3.12.0)
|
66
|
+
rubocop (1.56.2)
|
67
|
+
base64 (~> 0.1.1)
|
68
|
+
json (~> 2.3)
|
69
|
+
language_server-protocol (>= 3.17.0)
|
70
|
+
parallel (~> 1.10)
|
71
|
+
parser (>= 3.2.2.3)
|
72
|
+
rainbow (>= 2.2.2, < 4.0)
|
73
|
+
regexp_parser (>= 1.8, < 3.0)
|
74
|
+
rexml (>= 3.2.5, < 4.0)
|
75
|
+
rubocop-ast (>= 1.28.1, < 2.0)
|
76
|
+
ruby-progressbar (~> 1.7)
|
77
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
78
|
+
rubocop-ast (1.29.0)
|
79
|
+
parser (>= 3.2.1.0)
|
80
|
+
ruby-progressbar (1.13.0)
|
81
|
+
securerandom (0.2.2)
|
82
|
+
snaky_hash (2.0.1)
|
83
|
+
hashie
|
84
|
+
version_gem (~> 1.1, >= 1.1.1)
|
85
|
+
steep (1.5.3)
|
86
|
+
activesupport (>= 5.1)
|
87
|
+
concurrent-ruby (>= 1.1.10)
|
88
|
+
csv (>= 3.0.9)
|
89
|
+
fileutils (>= 1.1.0)
|
90
|
+
json (>= 2.1.0)
|
91
|
+
language_server-protocol (>= 3.15, < 4.0)
|
92
|
+
listen (~> 3.0)
|
93
|
+
logger (>= 1.3.0)
|
94
|
+
parser (>= 3.1)
|
95
|
+
rainbow (>= 2.2.2, < 4.0)
|
96
|
+
rbs (>= 3.1.0)
|
97
|
+
securerandom (>= 0.1)
|
98
|
+
strscan (>= 1.0.0)
|
99
|
+
terminal-table (>= 2, < 4)
|
100
|
+
strscan (3.0.6)
|
101
|
+
terminal-table (3.0.2)
|
102
|
+
unicode-display_width (>= 1.1.1, < 3)
|
103
|
+
tzinfo (2.0.6)
|
104
|
+
concurrent-ruby (~> 1.0)
|
105
|
+
unicode-display_width (2.4.2)
|
106
|
+
version_gem (1.1.3)
|
107
|
+
|
108
|
+
PLATFORMS
|
109
|
+
arm64-darwin-21
|
110
|
+
x86_64-linux
|
111
|
+
|
112
|
+
DEPENDENCIES
|
113
|
+
rake (~> 13.0)
|
114
|
+
rspec (~> 3.0)
|
115
|
+
rubocop (~> 1.56)
|
116
|
+
simple_tweet!
|
117
|
+
steep
|
118
|
+
|
119
|
+
BUNDLED WITH
|
120
|
+
2.3.24
|
data/Steepfile
ADDED
@@ -1,17 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "json"
|
4
|
+
require "cgi"
|
5
|
+
require "oauth"
|
6
|
+
require "net/http/post/multipart"
|
4
7
|
|
5
8
|
module SimpleTweet
|
6
9
|
module V2
|
7
|
-
#
|
8
|
-
class Client
|
9
|
-
|
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"
|
10
15
|
TW_METADATA_CREATE_PATH = "/1.1/media/metadata/create.json"
|
16
|
+
TW_TWEET_PATH = "/2/tweets"
|
11
17
|
UA = "SimpleTweet/#{SimpleTweet::VERSION}".freeze
|
18
|
+
APPEND_PER = 5 * (1 << 20)
|
12
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
|
27
|
+
|
28
|
+
# https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/migrate
|
13
29
|
def tweet(message:, media_ids: [])
|
14
|
-
json = { text: message }
|
30
|
+
json = { text: message } # : ::Hash[::Symbol, (::String|::Hash[::Symbol, ::Array[::String]])]
|
15
31
|
json[:media] = { media_ids: media_ids } unless media_ids.empty?
|
16
32
|
header = { "User-Agent": UA, "content-type": "application/json" }
|
17
33
|
access_token.post(TW_TWEET_PATH, json.to_json, header)
|
@@ -29,12 +45,135 @@ module SimpleTweet
|
|
29
45
|
|
30
46
|
private
|
31
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(
|
59
|
+
url.host, # : ::String
|
60
|
+
url.port
|
61
|
+
)
|
62
|
+
https.use_ssl = true
|
63
|
+
|
64
|
+
https.start do |http|
|
65
|
+
http.request req
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload
|
70
|
+
## maybe todo: multiple image
|
71
|
+
# ここはv1のAPIを叩いている。
|
72
|
+
def upload_media(media_type:, media:)
|
73
|
+
return upload_video(video: media) if media_type == "video/mp4"
|
74
|
+
|
75
|
+
req = ::Net::HTTP::Post::Multipart.new(
|
76
|
+
TW_MEDIA_UPLOAD_PATH,
|
77
|
+
media: ::UploadIO.new(media, media_type),
|
78
|
+
media_category: "tweet_image"
|
79
|
+
)
|
80
|
+
res = ::JSON.parse(request(req).body)
|
81
|
+
[res["media_id_string"]]
|
82
|
+
end
|
83
|
+
|
84
|
+
def init(video:)
|
85
|
+
init_req = ::Net::HTTP::Post::Multipart.new(
|
86
|
+
TW_MEDIA_UPLOAD_PATH,
|
87
|
+
command: "INIT",
|
88
|
+
total_bytes: video.size,
|
89
|
+
media_type: "video/mp4"
|
90
|
+
)
|
91
|
+
init_res = request(init_req)
|
92
|
+
raise UploadMediaError.new("init failed", response: init_res) unless init_res.code == "202"
|
93
|
+
|
94
|
+
::JSON.parse(init_res.body)
|
95
|
+
end
|
96
|
+
|
97
|
+
def append(video:, media_id:, index:, retry_count: 0)
|
98
|
+
append_req = ::Net::HTTP::Post::Multipart.new(
|
99
|
+
TW_MEDIA_UPLOAD_PATH,
|
100
|
+
command: "APPEND",
|
101
|
+
media_id: media_id,
|
102
|
+
media: video.read(APPEND_PER),
|
103
|
+
segment_index: index
|
104
|
+
)
|
105
|
+
res = request(append_req)
|
106
|
+
return if res.code == "204"
|
107
|
+
raise UploadMediaError.new("append failed", response: res) unless retry_count <= @max_append_retry_
|
108
|
+
|
109
|
+
append(video: video, media_id: media_id, index: index, retry_count: retry_count + 1)
|
110
|
+
end
|
111
|
+
|
112
|
+
def finalize(media_id:)
|
113
|
+
finalize_req = ::Net::HTTP::Post::Multipart.new(
|
114
|
+
TW_MEDIA_UPLOAD_PATH,
|
115
|
+
command: "FINALIZE",
|
116
|
+
media_id: media_id
|
117
|
+
)
|
118
|
+
finalize_res = request(finalize_req)
|
119
|
+
raise UploadMediaError.new("finalize failed", response: finalize_res) unless finalize_res.code == "201"
|
120
|
+
|
121
|
+
::JSON.parse(finalize_res.body)
|
122
|
+
end
|
123
|
+
|
124
|
+
def status(media_id:)
|
125
|
+
status_req = ::Net::HTTP::Post::Multipart.new(
|
126
|
+
TW_MEDIA_UPLOAD_PATH,
|
127
|
+
command: "STATUS",
|
128
|
+
media_id: media_id
|
129
|
+
)
|
130
|
+
status_res = request(status_req)
|
131
|
+
raise UploadMediaError.new("status failed", response: status_res) unless status_res.code == "200"
|
132
|
+
|
133
|
+
::JSON.parse(status_res.body)
|
134
|
+
end
|
135
|
+
|
136
|
+
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload-init
|
137
|
+
def upload_video(video:)
|
138
|
+
init_res = init(video: video)
|
139
|
+
media_id = init_res["media_id_string"]
|
140
|
+
|
141
|
+
chunks_needed = (video.size - 1) / APPEND_PER + 1
|
142
|
+
chunks_needed.times do |i|
|
143
|
+
append(video: video, media_id: media_id, index: i)
|
144
|
+
end
|
145
|
+
|
146
|
+
finalize_res = finalize(media_id: media_id)
|
147
|
+
|
148
|
+
if finalize_res["processing_info"]
|
149
|
+
retry_after = finalize_res["processing_info"]["check_after_secs"] || 5
|
150
|
+
loop do
|
151
|
+
sleep retry_after
|
152
|
+
|
153
|
+
status_res = status(media_id: media_id)
|
154
|
+
raise UploadMediaError if status_res["processing_info"].nil?
|
155
|
+
break if status_res["processing_info"]["state"] == "succeeded"
|
156
|
+
|
157
|
+
if status_res["processing_info"]["state"] == "in_progress"
|
158
|
+
retry_after = status_res["processing_info"]["check_after_secs"] || 5
|
159
|
+
next
|
160
|
+
end
|
161
|
+
|
162
|
+
# status_res_json["processing_info"]["state"] == "failed"
|
163
|
+
raise UploadMediaError
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
[media_id]
|
168
|
+
end
|
169
|
+
|
32
170
|
def create_media_metadata(media_id:, alt_text:)
|
33
|
-
header = { "content-type"
|
171
|
+
header = { "content-type" => "application/json; charset=UTF-8" } # : ::Hash[::String, ::String]
|
34
172
|
req = ::Net::HTTP::Post.new(TW_METADATA_CREATE_PATH, header)
|
35
173
|
req.body = { media_id: media_id, alt_text: { text: alt_text } }.to_json
|
36
174
|
res = request(req)
|
37
|
-
|
175
|
+
raise UploadMediaError.new("create_media_metadata failed", response: res) if res.code != "200"
|
176
|
+
|
38
177
|
res
|
39
178
|
end
|
40
179
|
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) unless message.nil?
|
15
|
+
@response = response
|
130
16
|
end
|
131
17
|
end
|
132
18
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
---
|
2
|
+
sources:
|
3
|
+
- type: git
|
4
|
+
name: ruby/gem_rbs_collection
|
5
|
+
revision: 248499a924c3cb331d40ca667d40528814043788
|
6
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
7
|
+
repo_dir: gems
|
8
|
+
path: ".gem_rbs_collection"
|
9
|
+
gems:
|
10
|
+
- name: ast
|
11
|
+
version: '2.4'
|
12
|
+
source:
|
13
|
+
type: git
|
14
|
+
name: ruby/gem_rbs_collection
|
15
|
+
revision: 248499a924c3cb331d40ca667d40528814043788
|
16
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
17
|
+
repo_dir: gems
|
18
|
+
- name: base64
|
19
|
+
version: '0'
|
20
|
+
source:
|
21
|
+
type: stdlib
|
22
|
+
- name: hashie
|
23
|
+
version: '5.0'
|
24
|
+
source:
|
25
|
+
type: git
|
26
|
+
name: ruby/gem_rbs_collection
|
27
|
+
revision: 248499a924c3cb331d40ca667d40528814043788
|
28
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
29
|
+
repo_dir: gems
|
30
|
+
- name: json
|
31
|
+
version: '0'
|
32
|
+
source:
|
33
|
+
type: stdlib
|
34
|
+
- name: parallel
|
35
|
+
version: '1.20'
|
36
|
+
source:
|
37
|
+
type: git
|
38
|
+
name: ruby/gem_rbs_collection
|
39
|
+
revision: 248499a924c3cb331d40ca667d40528814043788
|
40
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
41
|
+
repo_dir: gems
|
42
|
+
- name: rainbow
|
43
|
+
version: '3.0'
|
44
|
+
source:
|
45
|
+
type: git
|
46
|
+
name: ruby/gem_rbs_collection
|
47
|
+
revision: 248499a924c3cb331d40ca667d40528814043788
|
48
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
49
|
+
repo_dir: gems
|
50
|
+
gemfile_lock_path: Gemfile.lock
|
data/rbs_collection.yaml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
sources:
|
2
|
+
- type: git
|
3
|
+
name: ruby/gem_rbs_collection
|
4
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
5
|
+
revision: main
|
6
|
+
repo_dir: gems
|
7
|
+
|
8
|
+
# A directory to install the downloaded RBSs
|
9
|
+
path: .gem_rbs_collection
|
10
|
+
|
11
|
+
gems:
|
12
|
+
# Skip loading rbs gem's RBS.
|
13
|
+
# It's unnecessary if you don't use rbs as a library.
|
14
|
+
- name: rbs
|
15
|
+
ignore: true
|
16
|
+
- name: steep
|
17
|
+
ignore: true
|
data/sig/simple_tweet.rbs
CHANGED
@@ -1,4 +1,45 @@
|
|
1
1
|
module SimpleTweet
|
2
2
|
VERSION: String
|
3
|
-
|
3
|
+
|
4
|
+
module V2
|
5
|
+
class Client
|
6
|
+
TW_API_ORIGIN: String
|
7
|
+
TW_UPLOAD_ORIGIN: String
|
8
|
+
TW_MEDIA_UPLOAD_PATH: String
|
9
|
+
TW_METADATA_CREATE_PATH: String
|
10
|
+
TW_TWEET_PATH: String
|
11
|
+
UA: String
|
12
|
+
APPEND_PER: Integer
|
13
|
+
|
14
|
+
def initialize: (consumer_key: String, consumer_secret: String, access_token: String, access_token_secret: String, ?max_append_retry: Integer) -> void
|
15
|
+
def tweet: (message: String, ?media_ids: Array[String]) -> untyped
|
16
|
+
def tweet_with_media: (message: String, media_type: String, media: untyped, ?alt_text: String?) -> untyped
|
17
|
+
|
18
|
+
@consumer_key_: String
|
19
|
+
@consumer_secret_: String
|
20
|
+
@access_token_: String
|
21
|
+
@access_token_secret_: String
|
22
|
+
@max_append_retry_: Integer
|
23
|
+
@client: untyped # ::OAuth::AccessToken
|
24
|
+
|
25
|
+
private
|
26
|
+
def access_token: (?site: String) -> untyped
|
27
|
+
def request: (untyped req) -> untyped
|
28
|
+
def upload_media: (media_type: untyped, media: untyped) -> [untyped]
|
29
|
+
def init: (video: untyped) -> untyped
|
30
|
+
def append: (video: untyped, media_id: untyped, index: untyped, ?retry_count: Integer) -> nil
|
31
|
+
def finalize: (media_id: untyped) -> untyped
|
32
|
+
def status: (media_id: untyped) -> untyped
|
33
|
+
def upload_video: (video: untyped) -> [untyped]
|
34
|
+
def create_media_metadata: (media_id: String, alt_text: String?) -> untyped
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Error < StandardError
|
39
|
+
end
|
40
|
+
|
41
|
+
class UploadMediaError < Error
|
42
|
+
attr_reader response: nil
|
43
|
+
def initialize: (?String? message, ?response: nil) -> void
|
44
|
+
end
|
4
45
|
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.1
|
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-09-
|
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,13 +49,16 @@ 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
|
56
|
+
- Steepfile
|
55
57
|
- lib/simple_tweet.rb
|
56
|
-
- lib/simple_tweet/v1_client.rb
|
57
58
|
- lib/simple_tweet/v2_client.rb
|
58
59
|
- lib/simple_tweet/version.rb
|
60
|
+
- rbs_collection.lock.yaml
|
61
|
+
- rbs_collection.yaml
|
59
62
|
- sig/simple_tweet.rbs
|
60
63
|
- simple_tweet.gemspec
|
61
64
|
homepage: https://github.com/nota/simple_tweet
|
@@ -1,158 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "cgi"
|
4
|
-
require "oauth"
|
5
|
-
require "json"
|
6
|
-
require "net/http/post/multipart"
|
7
|
-
|
8
|
-
module SimpleTweet
|
9
|
-
module V1
|
10
|
-
# Client provides only tweet
|
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
|
-
APPEND_PER = 5 * (1 << 20)
|
16
|
-
|
17
|
-
def initialize(consumer_key:, consumer_secret:, access_token:, access_token_secret:, max_append_retry: 3)
|
18
|
-
@consumer_key_ = consumer_key
|
19
|
-
@consumer_secret_ = consumer_secret
|
20
|
-
@access_token_ = access_token
|
21
|
-
@access_token_secret_ = access_token_secret
|
22
|
-
@max_append_retry_ = max_append_retry
|
23
|
-
end
|
24
|
-
|
25
|
-
# https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-update
|
26
|
-
def tweet(message:, media_ids: [])
|
27
|
-
path = "/1.1/statuses/update.json?status=#{::CGI.escape(message)}"
|
28
|
-
path += "&media_ids=#{media_ids.join(",")}" unless media_ids.empty?
|
29
|
-
Tweet.from_response(access_token.post(path))
|
30
|
-
end
|
31
|
-
|
32
|
-
# media_type is mime_type
|
33
|
-
def tweet_with_media(message:, media_type:, media:)
|
34
|
-
media_ids = upload_media(media_type: media_type, media: media)
|
35
|
-
tweet(message: message, media_ids: media_ids)
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def access_token(site: TW_API_ORIGIN)
|
41
|
-
consumer = ::OAuth::Consumer.new(@consumer_key_, @consumer_secret_, site: site)
|
42
|
-
::OAuth::AccessToken.new(consumer, @access_token_, @access_token_secret_)
|
43
|
-
end
|
44
|
-
|
45
|
-
def request(req)
|
46
|
-
@client ||= access_token(site: TW_UPLOAD_ORIGIN)
|
47
|
-
@client.sign! req
|
48
|
-
|
49
|
-
url = ::URI.parse(TW_UPLOAD_ORIGIN + TW_MEDIA_UPLOAD_PATH)
|
50
|
-
https = ::Net::HTTP.new(url.host, url.port)
|
51
|
-
https.use_ssl = true
|
52
|
-
|
53
|
-
https.start do |http|
|
54
|
-
http.request req
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload
|
59
|
-
## maybe todo: multiple image
|
60
|
-
def upload_media(media_type:, media:)
|
61
|
-
return upload_video(video: media) if media_type == "video/mp4"
|
62
|
-
|
63
|
-
req = ::Net::HTTP::Post::Multipart.new(
|
64
|
-
TW_MEDIA_UPLOAD_PATH,
|
65
|
-
media: ::UploadIO.new(media, media_type),
|
66
|
-
media_category: "tweet_image"
|
67
|
-
)
|
68
|
-
res = ::JSON.parse(request(req).body)
|
69
|
-
[res["media_id_string"]]
|
70
|
-
end
|
71
|
-
|
72
|
-
def init(video:)
|
73
|
-
init_req = ::Net::HTTP::Post::Multipart.new(
|
74
|
-
TW_MEDIA_UPLOAD_PATH,
|
75
|
-
command: "INIT",
|
76
|
-
total_bytes: video.size,
|
77
|
-
media_type: "video/mp4"
|
78
|
-
)
|
79
|
-
init_res = request(init_req)
|
80
|
-
raise UploadMediaError unless init_res.code == "202"
|
81
|
-
|
82
|
-
::JSON.parse(init_res.body)
|
83
|
-
end
|
84
|
-
|
85
|
-
def append(video:, media_id:, index:, retry_count: 0)
|
86
|
-
append_req = ::Net::HTTP::Post::Multipart.new(
|
87
|
-
TW_MEDIA_UPLOAD_PATH,
|
88
|
-
command: "APPEND",
|
89
|
-
media_id: media_id,
|
90
|
-
media: video.read(APPEND_PER),
|
91
|
-
segment_index: index
|
92
|
-
)
|
93
|
-
return if request(append_req).code == "204"
|
94
|
-
raise UploadMediaError unless retry_count <= @max_append_retry_
|
95
|
-
|
96
|
-
append(video: video, media_id: media_id, index: index, retry_count: retry_count + 1)
|
97
|
-
end
|
98
|
-
|
99
|
-
def finalize(media_id:)
|
100
|
-
finalize_req = ::Net::HTTP::Post::Multipart.new(
|
101
|
-
TW_MEDIA_UPLOAD_PATH,
|
102
|
-
command: "FINALIZE",
|
103
|
-
media_id: media_id
|
104
|
-
)
|
105
|
-
finalize_res = request(finalize_req)
|
106
|
-
raise UploadMediaError unless finalize_res.code == "201"
|
107
|
-
|
108
|
-
::JSON.parse(finalize_res.body)
|
109
|
-
end
|
110
|
-
|
111
|
-
def status(media_id:)
|
112
|
-
status_req = ::Net::HTTP::Post::Multipart.new(
|
113
|
-
TW_MEDIA_UPLOAD_PATH,
|
114
|
-
command: "STATUS",
|
115
|
-
media_id: media_id
|
116
|
-
)
|
117
|
-
status_res = request(status_req)
|
118
|
-
raise UploadMediaError unless status_res.code == "200"
|
119
|
-
|
120
|
-
::JSON.parse(status_res.body)
|
121
|
-
end
|
122
|
-
|
123
|
-
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload-init
|
124
|
-
def upload_video(video:)
|
125
|
-
init_res = init(video: video)
|
126
|
-
media_id = init_res["media_id_string"]
|
127
|
-
|
128
|
-
chunks_needed = (video.size - 1) / APPEND_PER + 1
|
129
|
-
chunks_needed.times do |i|
|
130
|
-
append(video: video, media_id: media_id, index: i)
|
131
|
-
end
|
132
|
-
|
133
|
-
finalize_res = finalize(media_id: media_id)
|
134
|
-
|
135
|
-
if finalize_res["processing_info"]
|
136
|
-
retry_after = finalize_res["processing_info"]["check_after_secs"] || 5
|
137
|
-
loop do
|
138
|
-
sleep retry_after
|
139
|
-
|
140
|
-
status_res = status(media_id: media_id)
|
141
|
-
raise UploadMediaError if status_res["processing_info"].nil?
|
142
|
-
break if status_res["processing_info"]["state"] == "succeeded"
|
143
|
-
|
144
|
-
if status_res["processing_info"]["state"] == "in_progress"
|
145
|
-
retry_after = status_res["processing_info"]["check_after_secs"] || 5
|
146
|
-
next
|
147
|
-
end
|
148
|
-
|
149
|
-
# status_res_json["processing_info"]["state"] == "failed"
|
150
|
-
raise UploadMediaError
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
[media_id]
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
end
|