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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 824d9a2b160763d55494fef995e0e63974afd881c07098a96f2e3acc19a9439e
4
- data.tar.gz: c1c5050eaa9edce06a831c81202405daad44196454528295e759c83e1947ab5d
3
+ metadata.gz: a6ce5d4a7a448770b21c18cc4b68f25adc7f21d8814a02970864efc4c487d071
4
+ data.tar.gz: 6dfec2f42eff6f04cd8e0dc59571b31bc558b027fc20efdc87c59dd8402a47a1
5
5
  SHA512:
6
- metadata.gz: b782ae0dae6a0f7e54ba7985c1950def54e285d57da83334abb9a3a32d9b2e848c9525190ce058f1f30ee04f1e78edb2aa91361ee5461c40ae3ef80b9e0ee160
7
- data.tar.gz: '0980b93e87c9e7b9ff278db95c0748d6ac4ec02f5fb7bcd8fee132e935886d13b4b992b34eccf574cc411fcc278e3bed852068007dcf1aa9842daf60f49a9f04'
6
+ metadata.gz: bef5b277879212fc6c8a6c542ed70d71fd35e5c8e12ab0ab9119d50c3969e271b99b95f812b990eb4bb5ef93c9bc6b9726cb6e71fcf974112789e20da94e0b6f
7
+ data.tar.gz: '09771d8edada2fba52358e1f897410a620703d91fe87a6f1b602ec846c56e9771b4a3289908d4bcded74da60c396a235d074ae3388e61917da067422852346f5'
data/.rubocop.yml CHANGED
@@ -13,10 +13,13 @@ 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
20
20
 
21
21
  Metrics/AbcSize:
22
22
  Max: 30
23
+
24
+ Style/FrozenStringLiteralComment:
25
+ Enabled: false
data/Gemfile CHANGED
@@ -6,7 +6,6 @@ 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"
11
+ gem "steep"
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
@@ -0,0 +1,11 @@
1
+ D = Steep::Diagnostic
2
+
3
+ target :lib do
4
+ signature "sig"
5
+ check "lib"
6
+ library "net-http"
7
+
8
+ configure_code_diagnostics(D::Ruby.strict) do |config|
9
+ config[D::Ruby::UnknownConstant] = :information
10
+ end
11
+ end
@@ -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
- # 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
- 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": "application/json; charset=UTF-8" }
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
- throw UploadMediaError, "create_media_metadata failed: #{res.code} #{res.body}" if res.code != "200"
175
+ raise UploadMediaError.new("create_media_metadata failed", response: res) if res.code != "200"
176
+
38
177
  res
39
178
  end
40
179
  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.1"
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) 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
@@ -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
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
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: 2.3.0
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-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,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