simple_tweet 2.3.0 → 3.0.1

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: 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