simple_tweet 2.3.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 824d9a2b160763d55494fef995e0e63974afd881c07098a96f2e3acc19a9439e
4
- data.tar.gz: c1c5050eaa9edce06a831c81202405daad44196454528295e759c83e1947ab5d
3
+ metadata.gz: cb994313a55f3a80782b1c73c983a7551fa2a394f49aa5149efdf59372d20d40
4
+ data.tar.gz: 2dff9773d979e4f907b1ad37072adc59d30d13c64d73ed26a004032a50cc2456
5
5
  SHA512:
6
- metadata.gz: b782ae0dae6a0f7e54ba7985c1950def54e285d57da83334abb9a3a32d9b2e848c9525190ce058f1f30ee04f1e78edb2aa91361ee5461c40ae3ef80b9e0ee160
7
- data.tar.gz: '0980b93e87c9e7b9ff278db95c0748d6ac4ec02f5fb7bcd8fee132e935886d13b4b992b34eccf574cc411fcc278e3bed852068007dcf1aa9842daf60f49a9f04'
6
+ metadata.gz: e5425884322fcb27da54bf4747c709ad412c9479daf0fd0b040209dbbec8be6f5d930505b4a72c85dabbef8a55b5817b5f5d83f7be2a8255d4e2aaa9253ec4e3
7
+ data.tar.gz: 31fb1439304050a6038b19ce25520795eb3588ebcdfe29b75b1249adbbdfff932f79833c131ef3492ff784599f2e4e3301222f7885784d60de2a634652d6cbef
data/.rubocop.yml CHANGED
@@ -13,7 +13,7 @@ Layout/LineLength:
13
13
  Max: 120
14
14
 
15
15
  Metrics/ClassLength:
16
- Max: 120
16
+ Max: 150
17
17
 
18
18
  Metrics/MethodLength:
19
19
  Max: 30
data/Gemfile CHANGED
@@ -6,7 +6,5 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "rake", "~> 13.0"
9
-
10
9
  gem "rspec", "~> 3.0"
11
-
12
- gem "rubocop", "~> 1.21"
10
+ gem "rubocop", "~> 1.56"
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,15 +1,31 @@
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
- # mediaのuploadはapi 1.1しか用意されていないため、それを使う。
8
- class Client < V1::Client
9
- TW_TWEET_PATH = "/2/tweets"
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
30
  json = { text: message }
15
31
  json[:media] = { media_ids: media_ids } unless media_ids.empty?
@@ -29,12 +45,132 @@ 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(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
+
32
167
  def create_media_metadata(media_id:, alt_text:)
33
168
  header = { "content-type": "application/json; charset=UTF-8" }
34
169
  req = ::Net::HTTP::Post.new(TW_METADATA_CREATE_PATH, header)
35
170
  req.body = { media_id: media_id, alt_text: { text: alt_text } }.to_json
36
171
  res = request(req)
37
- throw UploadMediaError, "create_media_metadata failed: #{res.code} #{res.body}" if res.code != "200"
172
+ raise UploadMediaError.new("create_media_metadata failed", response: res) if res.code != "200"
173
+
38
174
  res
39
175
  end
40
176
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleTweet
4
- VERSION = "2.3.0"
4
+ VERSION = "3.0.0"
5
5
  end
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
- # Twitter::Tweetに近いinterfaceを提供する。
13
- ## 使ってない部分のhashの中身のhashとかは正規化されてないので、必要になったら足す必要がある。
14
- ### todo: entities, media, ...
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
- # Tweet is like Twitter::Tweet
95
- class Tweet
96
- def self.from_response(response)
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: 2.3.0
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-09-01 00:00:00.000000000 Z
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,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