simple_tweet 0.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/lib/simple_tweet/v1_client.rb +156 -0
- data/lib/simple_tweet/v2_client.rb +21 -0
- data/lib/simple_tweet/version.rb +1 -1
- data/lib/simple_tweet.rb +5 -151
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2348deca66984de880a63af33816583712ddcbb9b56237ce4c0974be3343cef4
|
4
|
+
data.tar.gz: 1681c985de747bd2192cdb78c1317148c9ac94d60fbc1e9ddd37b069f43a32cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 834a576f510f3f109a928977594cb03071bbe21d4b2c3f345b55ffc9681db1101025540642be6188a36f57b65763d284f33dbdc6392632fd53d696b5d368b34c
|
7
|
+
data.tar.gz: '09a28d5cdb58f8c71d50371a35129e1f126c9b6ca7b864af916f0fe0a1736ac0266faf26594cab069c7da6af1365e1bd5a561c14a52e2e4c6a692a45f0ec1d54'
|
data/README.md
CHANGED
@@ -26,7 +26,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
26
26
|
|
27
27
|
## Contributing
|
28
28
|
|
29
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
29
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/nota/simple_tweet. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/nota/simple_tweet/blob/main/CODE_OF_CONDUCT.md).
|
30
30
|
|
31
31
|
## License
|
32
32
|
|
@@ -34,4 +34,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
34
34
|
|
35
35
|
## Code of Conduct
|
36
36
|
|
37
|
-
Everyone interacting in the SimpleTweet project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
37
|
+
Everyone interacting in the SimpleTweet project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nota/simple_tweet/blob/main/CODE_OF_CONDUCT.md).
|
@@ -0,0 +1,156 @@
|
|
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
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module SimpleTweet
|
4
|
+
module V2
|
5
|
+
# mediaのuploadはapi 1.1しか用意されていないため、それを使う。
|
6
|
+
class Client < V1::Client
|
7
|
+
TW_TWEET_PATH = "/2/tweets"
|
8
|
+
UA = "SimpleTweet/#{SimpleTweet::VERSION}"
|
9
|
+
|
10
|
+
def tweet(message:, media_ids: [])
|
11
|
+
json = {
|
12
|
+
text: message,
|
13
|
+
}
|
14
|
+
json[:media] = { media_ids: media_ids } unless media_ids.empty?
|
15
|
+
header = { "User-Agent": UA, "content-type": "application/json" }
|
16
|
+
res = access_token.post(TW_TWEET_PATH, json.to_json, header)
|
17
|
+
::JSON.parse(res.body)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/simple_tweet/version.rb
CHANGED
data/lib/simple_tweet.rb
CHANGED
@@ -1,163 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "simple_tweet/version"
|
4
|
-
|
5
|
-
|
4
|
+
require_relative "simple_tweet/v1_client"
|
5
|
+
require_relative "simple_tweet/v2_client"
|
6
6
|
require "json"
|
7
|
-
require "net/http/post/multipart"
|
8
7
|
|
9
8
|
module SimpleTweet
|
10
9
|
class Error < ::StandardError; end
|
11
10
|
class UploadMediaError < Error; end
|
12
11
|
|
13
|
-
# Client provides only tweet
|
14
|
-
class Client
|
15
|
-
TW_API_ORIGIN = "https://api.twitter.com"
|
16
|
-
TW_UPLOAD_ORIGIN = "https://upload.twitter.com"
|
17
|
-
TW_MEDIA_UPLOAD_PATH = "/1.1/media/upload.json"
|
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
|
27
|
-
|
28
|
-
# https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-update
|
29
|
-
def tweet(message:, media_ids: [])
|
30
|
-
path = "/1.1/statuses/update.json?status=#{::CGI.escape(message)}"
|
31
|
-
path += "&media_ids=#{media_ids.join(",")}" unless media_ids.empty?
|
32
|
-
Tweet.from_response(access_token.post(path))
|
33
|
-
end
|
34
|
-
|
35
|
-
# media_type is mime_type
|
36
|
-
def tweet_with_media(message:, media_type:, media:)
|
37
|
-
media_ids = upload_media(media_type: media_type, media: media)
|
38
|
-
tweet(message: message, media_ids: media_ids)
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
def access_token(site: TW_API_ORIGIN)
|
44
|
-
consumer = ::OAuth::Consumer.new(@consumer_key_, @consumer_secret_, site: site)
|
45
|
-
::OAuth::AccessToken.new(consumer, @access_token_, @access_token_secret_)
|
46
|
-
end
|
47
|
-
|
48
|
-
def request(req)
|
49
|
-
@client ||= access_token(site: TW_UPLOAD_ORIGIN)
|
50
|
-
@client.sign! req
|
51
|
-
|
52
|
-
url = ::URI.parse(TW_UPLOAD_ORIGIN + TW_MEDIA_UPLOAD_PATH)
|
53
|
-
https = ::Net::HTTP.new(url.host, url.port)
|
54
|
-
https.use_ssl = true
|
55
|
-
|
56
|
-
https.start do |http|
|
57
|
-
http.request req
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload
|
62
|
-
## maybe todo: multiple image
|
63
|
-
def upload_media(media_type:, media:)
|
64
|
-
return upload_video(video: media) if media_type == "video/mp4"
|
65
|
-
|
66
|
-
req = ::Net::HTTP::Post::Multipart.new(
|
67
|
-
TW_MEDIA_UPLOAD_PATH,
|
68
|
-
media: ::UploadIO.new(media, media_type),
|
69
|
-
media_category: "tweet_image"
|
70
|
-
)
|
71
|
-
res = ::JSON.parse(request(req).body)
|
72
|
-
[res["media_id_string"]]
|
73
|
-
end
|
74
|
-
|
75
|
-
def init(video:)
|
76
|
-
init_req = ::Net::HTTP::Post::Multipart.new(
|
77
|
-
TW_MEDIA_UPLOAD_PATH,
|
78
|
-
command: "INIT",
|
79
|
-
total_bytes: video.size,
|
80
|
-
media_type: "video/mp4"
|
81
|
-
)
|
82
|
-
init_res = request(init_req)
|
83
|
-
raise UploadMediaError unless init_res.code == "202"
|
84
|
-
|
85
|
-
::JSON.parse(init_res.body)
|
86
|
-
end
|
87
|
-
|
88
|
-
def append(video:, media_id:, index:, retry_count: 0)
|
89
|
-
append_req = ::Net::HTTP::Post::Multipart.new(
|
90
|
-
TW_MEDIA_UPLOAD_PATH,
|
91
|
-
command: "APPEND",
|
92
|
-
media_id: media_id,
|
93
|
-
media: video.read(APPEND_PER),
|
94
|
-
segment_index: index
|
95
|
-
)
|
96
|
-
return if request(append_req).code == "204"
|
97
|
-
raise UploadMediaError unless retry_count <= @max_append_retry_
|
98
|
-
|
99
|
-
append(video: video, media_id: media_id, index: index, retry_count: retry_count + 1)
|
100
|
-
end
|
101
|
-
|
102
|
-
def finalize(media_id:)
|
103
|
-
finalize_req = ::Net::HTTP::Post::Multipart.new(
|
104
|
-
TW_MEDIA_UPLOAD_PATH,
|
105
|
-
command: "FINALIZE",
|
106
|
-
media_id: media_id
|
107
|
-
)
|
108
|
-
finalize_res = request(finalize_req)
|
109
|
-
raise UploadMediaError unless finalize_res.code == "201"
|
110
|
-
|
111
|
-
::JSON.parse(finalize_res.body)
|
112
|
-
end
|
113
|
-
|
114
|
-
def status(media_id:)
|
115
|
-
status_req = ::Net::HTTP::Post::Multipart.new(
|
116
|
-
TW_MEDIA_UPLOAD_PATH,
|
117
|
-
command: "STATUS",
|
118
|
-
media_id: media_id
|
119
|
-
)
|
120
|
-
status_res = request(status_req)
|
121
|
-
raise UploadMediaError unless status_res.code == "200"
|
122
|
-
|
123
|
-
::JSON.parse(status_res.body)
|
124
|
-
end
|
125
|
-
|
126
|
-
# https://developer.twitter.com/en/docs/twitter-api/v1/media/upload-media/api-reference/post-media-upload-init
|
127
|
-
def upload_video(video:)
|
128
|
-
init_res = init(video: video)
|
129
|
-
media_id = init_res["media_id_string"]
|
130
|
-
|
131
|
-
chunks_needed = (video.size - 1) / APPEND_PER + 1
|
132
|
-
chunks_needed.times do |i|
|
133
|
-
append(video: video, media_id: media_id, index: i)
|
134
|
-
end
|
135
|
-
|
136
|
-
finalize_res = finalize(media_id: media_id)
|
137
|
-
|
138
|
-
if finalize_res["processing_info"]
|
139
|
-
retry_after = finalize_res["processing_info"]["check_after_secs"] || 5
|
140
|
-
loop do
|
141
|
-
sleep retry_after
|
142
|
-
|
143
|
-
status_res = status(media_id: media_id)
|
144
|
-
raise UploadMediaError if status_res["processing_info"].nil?
|
145
|
-
break if status_res["processing_info"]["state"] == "succeeded"
|
146
|
-
|
147
|
-
if status_res["processing_info"]["state"] == "in_progress"
|
148
|
-
retry_after = status_res["processing_info"]["check_after_secs"] || 5
|
149
|
-
next
|
150
|
-
end
|
151
|
-
|
152
|
-
# status_res_json["processing_info"]["state"] == "failed"
|
153
|
-
raise UploadMediaError
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
[media_id]
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
12
|
# Twitter::Tweetに近いinterfaceを提供する。
|
162
13
|
## 使ってない部分のhashの中身のhashとかは正規化されてないので、必要になったら足す必要がある。
|
163
14
|
### todo: entities, media, ...
|
@@ -188,6 +39,9 @@ module SimpleTweet
|
|
188
39
|
:lang,
|
189
40
|
:extended_entities,
|
190
41
|
:possibly_sensitive,
|
42
|
+
:quoted_status_id,
|
43
|
+
:quoted_status_id_str,
|
44
|
+
:quoted_status,
|
191
45
|
keyword_init: true
|
192
46
|
)
|
193
47
|
User = ::Struct.new(
|
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: 0.
|
4
|
+
version: 2.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-05-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: multipart-post
|
@@ -53,6 +53,8 @@ files:
|
|
53
53
|
- README.md
|
54
54
|
- Rakefile
|
55
55
|
- lib/simple_tweet.rb
|
56
|
+
- lib/simple_tweet/v1_client.rb
|
57
|
+
- lib/simple_tweet/v2_client.rb
|
56
58
|
- lib/simple_tweet/version.rb
|
57
59
|
- sig/simple_tweet.rbs
|
58
60
|
- simple_tweet.gemspec
|