simple_tweet 2.2.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: 7e8b7b5eef07f8a7108285eeb67aa7708fd0f810b49c497e4d2a2e7e988f671d
4
- data.tar.gz: f0501a28e4072c89f67944a7982394fd21ebd3fbc0e852dd18b5d3eb98fc0d97
3
+ metadata.gz: cb994313a55f3a80782b1c73c983a7551fa2a394f49aa5149efdf59372d20d40
4
+ data.tar.gz: 2dff9773d979e4f907b1ad37072adc59d30d13c64d73ed26a004032a50cc2456
5
5
  SHA512:
6
- metadata.gz: 38863d94493b2b4a71f0306327d7db02785e2118d852b5dd5508e642e9bbc0d8d453104ab08853da9b2ded125d49e782afa99913fe16f09726c0da5acad77d53
7
- data.tar.gz: 7e8e2ad9278bacef086651331964f40c1020257339e3599128903081cc5123ec40904597c6568a51bd10c7a10597ec16d52fab2a3bdc5c58aeadc97726ec7d48
6
+ metadata.gz: e5425884322fcb27da54bf4747c709ad412c9479daf0fd0b040209dbbec8be6f5d930505b4a72c85dabbef8a55b5817b5f5d83f7be2a8255d4e2aaa9253ec4e3
7
+ data.tar.gz: 31fb1439304050a6038b19ce25520795eb3588ebcdfe29b75b1249adbbdfff932f79833c131ef3492ff784599f2e4e3301222f7885784d60de2a634652d6cbef
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.6
2
+ TargetRubyVersion: 3.0
3
3
 
4
4
  Style/StringLiterals:
5
5
  Enabled: true
@@ -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,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
- # mediaのuploadはapi 1.1しか用意されていないため、それを使う。
6
- class Client < V1::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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleTweet
4
- VERSION = "2.2.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.2.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-06-15 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,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