simple_tweet 2.3.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 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