twitter_tweet_bot 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +172 -0
  3. data/lib/twitter_tweet_bot/api/access_token.rb +64 -0
  4. data/lib/twitter_tweet_bot/api/authorization/secure_code.rb +77 -0
  5. data/lib/twitter_tweet_bot/api/authorization.rb +71 -0
  6. data/lib/twitter_tweet_bot/api/http/base.rb +17 -0
  7. data/lib/twitter_tweet_bot/api/http/error.rb +24 -0
  8. data/lib/twitter_tweet_bot/api/http/get.rb +28 -0
  9. data/lib/twitter_tweet_bot/api/http/headers.rb +21 -0
  10. data/lib/twitter_tweet_bot/api/http/post.rb +46 -0
  11. data/lib/twitter_tweet_bot/api/http.rb +31 -0
  12. data/lib/twitter_tweet_bot/api/refresh_token.rb +55 -0
  13. data/lib/twitter_tweet_bot/api/tweet.rb +36 -0
  14. data/lib/twitter_tweet_bot/api/users_me.rb +36 -0
  15. data/lib/twitter_tweet_bot/api.rb +5 -0
  16. data/lib/twitter_tweet_bot/cache/caching.rb +32 -0
  17. data/lib/twitter_tweet_bot/cache/client_ext.rb +29 -0
  18. data/lib/twitter_tweet_bot/cache/configuration_ext.rb +18 -0
  19. data/lib/twitter_tweet_bot/cache/entity_ext/authorization.rb +16 -0
  20. data/lib/twitter_tweet_bot/cache/entity_ext/base.rb +31 -0
  21. data/lib/twitter_tweet_bot/cache/entity_ext/token.rb +16 -0
  22. data/lib/twitter_tweet_bot/cache/entity_ext.rb +2 -0
  23. data/lib/twitter_tweet_bot/cache/store.rb +41 -0
  24. data/lib/twitter_tweet_bot/cache.rb +13 -0
  25. data/lib/twitter_tweet_bot/client/api.rb +50 -0
  26. data/lib/twitter_tweet_bot/client/entity.rb +38 -0
  27. data/lib/twitter_tweet_bot/client.rb +17 -0
  28. data/lib/twitter_tweet_bot/configration.rb +32 -0
  29. data/lib/twitter_tweet_bot/entity/authorization.rb +15 -0
  30. data/lib/twitter_tweet_bot/entity/base.rb +32 -0
  31. data/lib/twitter_tweet_bot/entity/token.rb +17 -0
  32. data/lib/twitter_tweet_bot/entity/tweet.rb +19 -0
  33. data/lib/twitter_tweet_bot/entity/user.rb +20 -0
  34. data/lib/twitter_tweet_bot/entity.rb +4 -0
  35. data/lib/twitter_tweet_bot/version.rb +3 -0
  36. data/lib/twitter_tweet_bot.rb +35 -0
  37. metadata +91 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8a2b63d602240d7b56c182f40ec07306600818005a0b5d35b9cf6f8a93e405cf
4
+ data.tar.gz: 9c2b1c30135546c64319ddaf38bf75ee15184d9d3a7b0d42404c96380b036ba0
5
+ SHA512:
6
+ metadata.gz: 453ac53d4a7240829771bee7d8a94e5b1b1a5bfebdb26352fbe63579bdad9df20ac2da805da551f8a8d5f9a20ab9c3e6361dbcc5c8c96e8a14f42e3201d0d7c9
7
+ data.tar.gz: 6ae79d1b5ac9de5a26830eb28ba6ae8a62e0df6e85beda2ae9975b3d625bfab7495b5b5d6a704dfd66e5b6327b7e04406b86dfd3d8587d0140058889ce73922a
data/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # twitter_tweet_bot
2
+ Tweet Bot with Twitter's V2 API.<br/>
3
+ (by Twitter's V2 API / OAuth 2.0 with PCKE)
4
+
5
+ ## Getting Started
6
+
7
+ ```rb
8
+ # Gemfile
9
+ gem 'twitter_tweet_bot'
10
+ ```
11
+
12
+ Then run `bundle install`.
13
+
14
+ ## Usage
15
+
16
+ ```rb
17
+ require 'twitter_tweet_bot'
18
+
19
+ TwitterTweetBot.post_tweet(<ACEESS_TOKEN>, 'Yeah!')
20
+ ```
21
+
22
+ 1. Congiuration
23
+ 2. Issue Authorization URL
24
+ 3. Go to Authorization URL
25
+ 4. Fetch Access Token
26
+ 5. Post Tweet
27
+
28
+ <details>
29
+
30
+ <summary>Details</summary>
31
+
32
+ #### Step1. Congiuration
33
+
34
+ ```rb
35
+ require 'twitter_tweet_bot'
36
+
37
+ TwitterTweetBot.configure do |config|
38
+ # Twitter's Bot Name (any)
39
+ config.name = 'iambot'
40
+ # Twitter's Client ID
41
+ config.client_id = '*****'
42
+ # Twitter's Client Secret
43
+ config.client_secret = '*****'
44
+ # Redirect URL After Authorization
45
+ config.redirect_uri = 'https://example.com/twitter/callback'
46
+ # Twitter's App Scopes with OAuth 2.0
47
+ config.scopes = ['tweet.read', 'tweet.write', 'users.read', 'offline.access']
48
+ end
49
+ ```
50
+
51
+ #### Step2. Issue an authorization url
52
+
53
+ ```rb
54
+ authorization = TwitterTweetBot.authorization
55
+ # =>
56
+ # #<TwitterTweetBot::Entity::Authorization
57
+ # @code_verifier="*****",
58
+ # @state="***",
59
+ # @url="https://twitter.com/i/oauth2/authorize?response_type=code&redirect_uri=<YOUR_REDIRECT_URI>&client_id=<YOUR_CLIENT_ID>&scope=<SCOPES>&code_challenge=*****&code_challenge_method=S256&state=***">
60
+ ```
61
+
62
+ #### Step3. Rerirect (or Go) to `authorization.url`
63
+
64
+ And smash `"Authorize app"`.
65
+
66
+ If authorized, redirected to your `config.redirect_uri`.<br/>
67
+ Check **CODE** in Twitter's response.
68
+
69
+ ```
70
+ e.g. https://example.com/twitter/callback?state=***&code=*****
71
+ ```
72
+
73
+ #### Step4. Fetch an access token
74
+
75
+ ```rb
76
+ token = TwitterTweetBot.fetch_token('<CODE>', authorization.code_verifier)
77
+ # =>
78
+ # #<TwitterTweetBot::Entity::Token
79
+ # @access_token="<YOUR_ACCESS_TOKEN>",
80
+ # @expires_in=7200,
81
+ # @refresh_token="<YOUR_REFRESH_TOKEN>",
82
+ # @scope="tweet.write users.read tweet.read offline.access",
83
+ # @token_type="bearer">
84
+ ```
85
+
86
+ #### Step5. Post a tweet
87
+
88
+ ```rb
89
+ TwitterTweetBot.post_tweet(token.access_token, 'Yeah!')
90
+ # =>
91
+ # #<TwitterTweetBot::Entity::Tweet
92
+ # @edit_history_tweet_ids=["0123456789"],
93
+ # @id="0123456789",
94
+ # @text="Yeah!">
95
+ ```
96
+
97
+ #### Ex. Restore an access token (required `'offline.access'` in scopes)
98
+
99
+ ```rb
100
+ TwitterTweetBot.refresh_token(token.refresh_token)
101
+ ```
102
+ </details>
103
+
104
+ ### Caching
105
+
106
+ `TwitterTweetBot` can cache follow variables automatically in any store (like `Rails.cache`).
107
+
108
+ - `code_verifier`
109
+ - `state`
110
+ - `access_token`
111
+ - `refresh_token`
112
+
113
+ If needed, require `'twitter_tweet_bot/cache'`.
114
+
115
+ ```rb
116
+ require 'twitter_tweet_bot/cache'
117
+
118
+ TwitterTweetBot.post_tweet('Yeah!')
119
+ ```
120
+
121
+ <details>
122
+
123
+ <summary>Details</summary>
124
+
125
+ #### Step1. Congiuration
126
+
127
+ ```rb
128
+ require 'twitter_tweet_bot'
129
+
130
+ TwitterTweetBot.configure do |config|
131
+ # ...
132
+
133
+ # Any Cache Store (required `#write(key, value)` and `#read(key)` implementation).
134
+ config.cache_provider = ActiveSupport::Cache.lookup_store(:file_store, '../tmp/cache')
135
+ end
136
+ ```
137
+
138
+ #### Step2. Issue an authorization url
139
+
140
+ ```rb
141
+ # `code_verifier` and `state` will be cached.
142
+ TwitterTweetBot.authorization
143
+ ```
144
+
145
+ #### Step3. Fetch an access token
146
+
147
+
148
+ ```rb
149
+ # `access_token` and `refresh_token` will be cached.
150
+ TwitterTweetBot.fetch_token('<CODE>')
151
+ ```
152
+
153
+ Don't need to pass `code_verifier`.<br/>
154
+ (resolved from cache)
155
+
156
+ #### Step4. Post a tweet
157
+
158
+ ```rb
159
+ TwitterTweetBot.post_tweet('Yeah!')
160
+ ```
161
+
162
+ Don't need to pass `access_token`.<br/>
163
+ (resolved from cache)
164
+
165
+ #### Ex. Check a cache
166
+
167
+ ```rb
168
+ TwitterTweetBot.client.cache.read
169
+ # =>
170
+ # { :code_verifier=>"*****", :state=>"***", :access_token=>"*****", :refresh_token=>"*****" }
171
+ ```
172
+ </details>
@@ -0,0 +1,64 @@
1
+ require 'base64'
2
+ require 'twitter_tweet_bot/api/http'
3
+
4
+ module TwitterTweetBot
5
+ module API
6
+ class AccessToken
7
+ private_class_method :new
8
+
9
+ include HTTP
10
+
11
+ API_ENDPOTNT = 'https://api.twitter.com/2/oauth2/token'.freeze
12
+ GRANT_TYPE = 'authorization_code'.freeze
13
+
14
+ def self.fetch(
15
+ client_id:,
16
+ client_secret:,
17
+ redirect_uri:,
18
+ code:,
19
+ code_verifier:,
20
+ **
21
+ )
22
+ new(client_id, client_secret, redirect_uri)
23
+ .fetch(code, code_verifier)
24
+ end
25
+
26
+ def initialize(client_id, client_secret, redirect_uri)
27
+ @client_id = client_id
28
+ @client_secret = client_secret
29
+ @redirect_uri = redirect_uri
30
+ end
31
+
32
+ def fetch(code, code_verifier)
33
+ request(
34
+ :post_form,
35
+ API_ENDPOTNT,
36
+ body_with(code, code_verifier),
37
+ headers
38
+ )
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :client_id, :client_secret, :redirect_uri
44
+
45
+ def body_with(code, code_verifier)
46
+ {
47
+ grant_type: GRANT_TYPE,
48
+ code: code,
49
+ code_verifier: code_verifier,
50
+ redirect_uri: redirect_uri
51
+ }
52
+ end
53
+
54
+ def headers
55
+ basic_authorization_header(
56
+ Base64.strict_encode64("#{client_id}:#{client_secret}")
57
+ )
58
+ end
59
+
60
+ private_constant :API_ENDPOTNT,
61
+ :GRANT_TYPE
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,77 @@
1
+ require 'base64'
2
+ require 'securerandom'
3
+ require 'openssl'
4
+
5
+ module TwitterTweetBot
6
+ module API
7
+ class Authorization
8
+ class SecureCode
9
+ DEFAULT_CHALLENGE_SIZE = 64
10
+ DEFAULT_CHALLENGE_METHOD = 'S256'.freeze
11
+ DEFAULT_STATE_SIZE = 32
12
+
13
+ class << self
14
+ def code_verifier(size = DEFAULT_CHALLENGE_SIZE)
15
+ encode(random_chars(size))
16
+ end
17
+
18
+ def state(size = DEFAULT_STATE_SIZE)
19
+ encode(random_chars(size))
20
+ end
21
+
22
+ def code_challenge(verifier, challenge_method = DEFAULT_CHALLENGE_METHOD)
23
+ case challenge_method
24
+ when 'S256'
25
+ encode(
26
+ Base64.urlsafe_encode64(digest_by_sha256(verifier), padding: false)
27
+ )
28
+ else # 'plain'
29
+ verifier
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def random_chars(size)
36
+ SecureRandom.urlsafe_base64(size / 4 * 3, false)
37
+ end
38
+
39
+ def digest_by_sha256(chars)
40
+ OpenSSL::Digest::SHA256.digest(chars)
41
+ end
42
+
43
+ def encode(chars)
44
+ chars.encode(Encoding::UTF_8)
45
+ end
46
+ end
47
+
48
+ def initialize(code_verifier: nil, code_challenge_method: nil, state: nil)
49
+ @code_verifier = code_verifier
50
+ @code_challenge_method = code_challenge_method
51
+ @state = state
52
+ end
53
+
54
+ def code_verifier
55
+ @code_verifier ||= self.class.code_verifier
56
+ end
57
+
58
+ def code_challenge
59
+ @code_challenge ||= \
60
+ self.class.code_challenge(code_verifier, code_challenge_method)
61
+ end
62
+
63
+ def code_challenge_method
64
+ @code_challenge_method ||= DEFAULT_CHALLENGE_METHOD
65
+ end
66
+
67
+ def state
68
+ @state ||= self.class.state
69
+ end
70
+
71
+ private_constant :DEFAULT_CHALLENGE_SIZE,
72
+ :DEFAULT_CHALLENGE_METHOD,
73
+ :DEFAULT_STATE_SIZE
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,71 @@
1
+ require 'twitter_tweet_bot/api/authorization/secure_code'
2
+ require 'twitter_tweet_bot/api/http'
3
+
4
+ module TwitterTweetBot
5
+ module API
6
+ class Authorization
7
+ private_class_method :new
8
+
9
+ include HTTP
10
+
11
+ AUTH_URL = 'https://twitter.com/i/oauth2/authorize'.freeze
12
+ RESPONSE_TYPE = 'code'.freeze
13
+
14
+ def self.authorization(
15
+ client_id:,
16
+ redirect_uri:,
17
+ scopes:,
18
+ code_verifier:,
19
+ code_challenge_method:,
20
+ state:,
21
+ **
22
+ )
23
+ new(client_id, redirect_uri, scopes)
24
+ .authorization(code_verifier, code_challenge_method, state)
25
+ end
26
+
27
+ def initialize(client_id, redirect_uri, scopes)
28
+ @client_id = client_id
29
+ @redirect_uri = redirect_uri
30
+ @scopes = scopes
31
+ end
32
+
33
+ def authorization(code_verifier, code_challenge_method, state)
34
+ secure_code = Authorization::SecureCode.new(
35
+ code_verifier: code_verifier,
36
+ code_challenge_method: code_challenge_method,
37
+ state: state
38
+ )
39
+ uri = build_uri(AUTH_URL, build_body(secure_code))
40
+ return as_hash(uri.to_s, secure_code)
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :client_id, :redirect_uri, :scopes
46
+
47
+ def build_body(secure_code)
48
+ {
49
+ response_type: RESPONSE_TYPE,
50
+ redirect_uri: redirect_uri,
51
+ client_id: client_id,
52
+ scope: scopes.join(' '),
53
+ code_challenge: secure_code.code_challenge,
54
+ code_challenge_method: secure_code.code_challenge_method,
55
+ state: secure_code.state
56
+ }
57
+ end
58
+
59
+ def as_hash(url, secure_code)
60
+ {
61
+ url: url,
62
+ code_verifier: secure_code.code_verifier,
63
+ state: secure_code.state
64
+ }
65
+ end
66
+
67
+ private_constant :AUTH_URL,
68
+ :RESPONSE_TYPE
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,17 @@
1
+ require 'net/http'
2
+
3
+ module TwitterTweetBot
4
+ module API
5
+ module HTTP
6
+ module Base
7
+ def perform_request(uri, request)
8
+ Net::HTTP.start(
9
+ uri.host, uri.port, use_ssl: uri.scheme == 'https'
10
+ ) do |http|
11
+ http.request(request)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ module TwitterTweetBot
2
+ module API
3
+ module HTTP
4
+ module Error
5
+ class RequestFaild < StandardError
6
+ attr_reader :code, :body
7
+
8
+ def initialize(code, body)
9
+ @code = code
10
+ @body = body
11
+ end
12
+
13
+ def inspect
14
+ "#<#{self.class} #{code} #{body}>"
15
+ end
16
+ end
17
+
18
+ def request_error!(code, body)
19
+ raise RequestFaild.new(code, body)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'twitter_tweet_bot/api/http/base'
4
+
5
+ module TwitterTweetBot
6
+ module API
7
+ module HTTP
8
+ module Get
9
+ include Base
10
+
11
+ def request_get(url, body, headers = {})
12
+ uri = build_uri(url, body)
13
+ perform_request(
14
+ uri, Net::HTTP::Get.new(uri, headers)
15
+ )
16
+ end
17
+
18
+ private
19
+
20
+ def build_uri(url, body)
21
+ URI.parse(url).tap do |uri|
22
+ uri.query = URI.encode_www_form(body)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+
4
+ module TwitterTweetBot
5
+ module API
6
+ module HTTP
7
+ module Headers
8
+ BASIC_AUTHORIZATION = 'Basic %<credentials>s'.freeze
9
+ BEARER_AUTHORIZATION = 'Bearer %<credentials>s'.freeze
10
+
11
+ def basic_authorization_header(credentials)
12
+ { authorization: format(BASIC_AUTHORIZATION, credentials: credentials) }
13
+ end
14
+
15
+ def bearer_authorization_header(credentials)
16
+ { authorization: format(BEARER_AUTHORIZATION, credentials: credentials) }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,46 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'json'
4
+ require 'twitter_tweet_bot/api/http/base'
5
+
6
+ module TwitterTweetBot
7
+ module API
8
+ module HTTP
9
+ module Post
10
+ include Base
11
+
12
+ JSON_CONTENT_TYPE = 'application/json'.freeze
13
+ URLENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=UTF-8'.freeze
14
+
15
+ def request_post_form(url, body, headers = {})
16
+ request_post(
17
+ url,
18
+ URI.encode_www_form(body),
19
+ headers.merge('content-type' => URLENCODED_CONTENT_TYPE)
20
+ )
21
+ end
22
+
23
+ def request_post_json(url, body, headers = {})
24
+ request_post(
25
+ url,
26
+ body.to_json,
27
+ headers.merge('content-type' => JSON_CONTENT_TYPE)
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def request_post(url, body, headers)
34
+ uri = URI.parse(url)
35
+ perform_request(
36
+ uri,
37
+ Net::HTTP::Post.new(uri.path, headers).tap { |req| req.body = body }
38
+ )
39
+ end
40
+
41
+ private_constant :JSON_CONTENT_TYPE,
42
+ :URLENCODED_CONTENT_TYPE
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,31 @@
1
+ require 'twitter_tweet_bot/api/http/error'
2
+ require 'twitter_tweet_bot/api/http/headers'
3
+ require 'twitter_tweet_bot/api/http/get'
4
+ require 'twitter_tweet_bot/api/http/post'
5
+
6
+ module TwitterTweetBot
7
+ module API
8
+ module HTTP
9
+ include Error
10
+ include Headers
11
+ include Get
12
+ include Post
13
+
14
+ def request(method, url, body, headers)
15
+ response = send("request_#{method}", url, body, headers)
16
+ success_or_fail!(response)
17
+ end
18
+
19
+ private
20
+
21
+ def success_or_fail!(response)
22
+ case response
23
+ when Net::HTTPSuccess, Net::HTTPRedirection
24
+ response.body
25
+ else
26
+ request_error!(response.code, response.body)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,55 @@
1
+ require 'base64'
2
+ require 'twitter_tweet_bot/api/http'
3
+
4
+ module TwitterTweetBot
5
+ module API
6
+ class RefreshToken
7
+ private_class_method :new
8
+
9
+ include HTTP
10
+
11
+ API_ENDPOTNT = 'https://api.twitter.com/2/oauth2/token'.freeze
12
+ GRANT_TYPE = 'refresh_token'.freeze
13
+
14
+ def self.fetch(client_id:, client_secret:, refresh_token:, **)
15
+ new(client_id, client_secret)
16
+ .fetch(refresh_token)
17
+ end
18
+
19
+ def initialize(client_id, client_secret)
20
+ @client_id = client_id
21
+ @client_secret = client_secret
22
+ end
23
+
24
+ def fetch(refresh_token)
25
+ request(
26
+ :post_form,
27
+ API_ENDPOTNT,
28
+ body_with(refresh_token),
29
+ headers
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :client_id, :client_secret
36
+
37
+ def body_with(refresh_token)
38
+ {
39
+ grant_type: GRANT_TYPE,
40
+ refresh_token: refresh_token,
41
+ client_id: client_id
42
+ }
43
+ end
44
+
45
+ def headers
46
+ basic_authorization_header(
47
+ Base64.strict_encode64("#{client_id}:#{client_secret}")
48
+ )
49
+ end
50
+
51
+ private_constant :API_ENDPOTNT,
52
+ :GRANT_TYPE
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,36 @@
1
+ require 'twitter_tweet_bot/api/http'
2
+
3
+ module TwitterTweetBot
4
+ module API
5
+ class Tweet
6
+ private_class_method :new
7
+
8
+ include HTTP
9
+
10
+ API_ENDPOTNT = 'https://api.twitter.com/2/tweets'.freeze
11
+
12
+ def self.post(access_token:, text:, **)
13
+ new(access_token).post(text)
14
+ end
15
+
16
+ def initialize(access_token)
17
+ @access_token = access_token
18
+ end
19
+
20
+ def post(text)
21
+ request(
22
+ :post_json,
23
+ API_ENDPOTNT,
24
+ { text: text },
25
+ bearer_authorization_header(access_token)
26
+ )
27
+ end
28
+
29
+ private
30
+
31
+ attr_accessor :access_token
32
+
33
+ private_constant :API_ENDPOTNT
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ require 'twitter_tweet_bot/api/http'
2
+
3
+ module TwitterTweetBot
4
+ module API
5
+ class UsersMe
6
+ private_class_method :new
7
+
8
+ include HTTP
9
+
10
+ API_ENDPOTNT = 'https://api.twitter.com/2/users/me'.freeze
11
+
12
+ def self.fetch(access_token:, fields:, **)
13
+ new(access_token).fetch(fields)
14
+ end
15
+
16
+ def initialize(access_token)
17
+ @access_token = access_token
18
+ end
19
+
20
+ def fetch(fields)
21
+ request(
22
+ :get,
23
+ API_ENDPOTNT,
24
+ fields,
25
+ bearer_authorization_header(access_token)
26
+ )
27
+ end
28
+
29
+ private
30
+
31
+ attr_accessor :access_token
32
+
33
+ private_constant :API_ENDPOTNT
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ require 'twitter_tweet_bot/api/access_token'
2
+ require 'twitter_tweet_bot/api/authorization'
3
+ require 'twitter_tweet_bot/api/refresh_token'
4
+ require 'twitter_tweet_bot/api/tweet'
5
+ require 'twitter_tweet_bot/api/users_me'
@@ -0,0 +1,32 @@
1
+ module TwitterTweetBot
2
+ module Cache
3
+ module Caching
4
+ def cache
5
+ config.cache
6
+ end
7
+
8
+ def with_cache(&block)
9
+ current_cache = read_cache
10
+ result = yield(current_cache)
11
+ write_cache(current_cache, result)
12
+
13
+ result
14
+ end
15
+
16
+ private
17
+
18
+ def read_cache
19
+ cache.read || {}
20
+ end
21
+
22
+ def write_cache(current, value)
23
+ cache.write(current.merge(**to_cache_object(value)))
24
+ end
25
+
26
+ def to_cache_object(object)
27
+ return {} unless object.respond_to?(:to_cache)
28
+ object.to_cache
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ require 'twitter_tweet_bot/cache/caching'
2
+
3
+ module TwitterTweetBot
4
+ module Cache
5
+ module ClientExt
6
+ include Caching
7
+
8
+ def authorization(*)
9
+ with_cache { super }
10
+ end
11
+
12
+ def fetch_token(*args)
13
+ with_cache { |cache| super(*args, cache[:code_verifier]) }
14
+ end
15
+
16
+ def refresh_token
17
+ with_cache { |cache| super(cache[:refresh_token]) }
18
+ end
19
+
20
+ def post_tweet(*args)
21
+ with_cache { |cache| super(cache[:access_token], *args) }
22
+ end
23
+
24
+ def users_me(*args)
25
+ with_cache { |cache| super(cache[:access_token], *args) }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ require 'twitter_tweet_bot/cache/store'
2
+
3
+ module TwitterTweetBot
4
+ module Cache
5
+ module ConfigrationExt
6
+ attr_accessor :cache_provider, :cache
7
+
8
+ def initialize(cache_provider: nil, **kwargs)
9
+ @cache_provider = cache_provider
10
+ super(**kwargs)
11
+ end
12
+
13
+ def cache
14
+ @cache ||= Store.new(name, cache_provider)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ require 'twitter_tweet_bot/cache/entity_ext/base'
2
+
3
+ module TwitterTweetBot
4
+ module Cache
5
+ module EntityExt
6
+ module Authorization
7
+ include Base
8
+
9
+ act_as_cache_entity(
10
+ :code_verifier,
11
+ :state
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/module/attribute_accessors'
3
+
4
+ module TwitterTweetBot
5
+ module Cache
6
+ module EntityExt
7
+ module Base
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ def act_as_cache_entity(*cache_fields)
12
+ mattr_reader :cache_fields,
13
+ default: cache_fields
14
+ end
15
+ end
16
+
17
+ def to_cache
18
+ cache_fields.reduce({}) do |hash, cache_field|
19
+ next hash unless respond_to?(cache_field)
20
+
21
+ value = send(cache_field)
22
+ next hash if value.nil?
23
+ hash[cache_field] = value
24
+
25
+ hash
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ require 'twitter_tweet_bot/cache/entity_ext/base'
2
+
3
+ module TwitterTweetBot
4
+ module Cache
5
+ module EntityExt
6
+ module Token
7
+ include Base
8
+
9
+ act_as_cache_entity(
10
+ :access_token,
11
+ :refresh_token
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,2 @@
1
+ require 'twitter_tweet_bot/cache/entity_ext/authorization'
2
+ require 'twitter_tweet_bot/cache/entity_ext/token'
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+
3
+ module TwitterTweetBot
4
+ module Cache
5
+ class Store
6
+ CACHE_BASE_KEY = 'twitter_tweet_bot'.freeze
7
+
8
+ def initialize(name, provider)
9
+ @name = name
10
+ @provider = provider
11
+ end
12
+
13
+ def write(value)
14
+ provider.write(cache_key, serialize_value(value))
15
+ end
16
+
17
+ def read
18
+ deserialize_value(provider.read(cache_key))
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :name, :provider
24
+
25
+ def cache_key
26
+ File.join(CACHE_BASE_KEY, name.to_s)
27
+ end
28
+
29
+ def serialize_value(value)
30
+ value.to_json
31
+ end
32
+
33
+ def deserialize_value(value)
34
+ return {} if value.nil?
35
+ JSON.parse(value, symbolize_names: true)
36
+ end
37
+
38
+ private_constant :CACHE_BASE_KEY
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ require 'twitter_tweet_bot'
2
+
3
+ require 'twitter_tweet_bot/cache/client_ext'
4
+ require 'twitter_tweet_bot/cache/configuration_ext'
5
+ require 'twitter_tweet_bot/cache/entity_ext'
6
+
7
+ module TwitterTweetBot
8
+ Client.prepend(Cache::ClientExt)
9
+ Configration.prepend(Cache::ConfigrationExt)
10
+
11
+ Entity::Authorization.include(Cache::EntityExt::Authorization)
12
+ Entity::Token.include(Cache::EntityExt::Token)
13
+ end
@@ -0,0 +1,50 @@
1
+ require 'twitter_tweet_bot/api'
2
+
3
+ module TwitterTweetBot
4
+ class Client
5
+ module API
6
+ def authorization(code_verifier = nil, code_challenge_method = nil, state = nil)
7
+ TwitterTweetBot::API::Authorization.authorization(
8
+ **params_with_config(
9
+ code_verifier: code_verifier,
10
+ code_challenge_method: code_challenge_method,
11
+ state: state
12
+ )
13
+ )
14
+ end
15
+
16
+ def fetch_token(code, code_verifier)
17
+ TwitterTweetBot::API::AccessToken.fetch(
18
+ **params_with_config(
19
+ code: code,
20
+ code_verifier: code_verifier
21
+ )
22
+ )
23
+ end
24
+
25
+ def refresh_token(refresh_token)
26
+ TwitterTweetBot::API::RefreshToken.fetch(
27
+ **params_with_config(refresh_token: refresh_token)
28
+ )
29
+ end
30
+
31
+ def post_tweet(access_token, text)
32
+ TwitterTweetBot::API::Tweet.post(
33
+ **params_with_config(access_token: access_token, text: text)
34
+ )
35
+ end
36
+
37
+ def users_me(access_token, fields = {})
38
+ TwitterTweetBot::API::UsersMe.fetch(
39
+ **params_with_config(access_token: access_token, fields: fields)
40
+ )
41
+ end
42
+
43
+ private
44
+
45
+ def params_with_config(**params)
46
+ config.to_hash.merge(**params)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,38 @@
1
+ require 'json'
2
+ require 'twitter_tweet_bot/entity'
3
+
4
+ module TwitterTweetBot
5
+ class Client
6
+ module Entity
7
+ def authorization(*)
8
+ with_entity(TwitterTweetBot::Entity::Authorization) { super }
9
+ end
10
+
11
+ def fetch_token(*)
12
+ with_entity(TwitterTweetBot::Entity::Token) { parse_json(super) }
13
+ end
14
+
15
+ def refresh_token(*)
16
+ with_entity(TwitterTweetBot::Entity::Token) { parse_json(super) }
17
+ end
18
+
19
+ def post_tweet(*)
20
+ with_entity(TwitterTweetBot::Entity::Tweet) { parse_json(super) }
21
+ end
22
+
23
+ def users_me(*)
24
+ with_entity(TwitterTweetBot::Entity::User) { parse_json(super) }
25
+ end
26
+
27
+ private
28
+
29
+ def with_entity(entity_klass, &block)
30
+ entity_klass.new(yield)
31
+ end
32
+
33
+ def parse_json(body)
34
+ JSON.parse(body, symbolize_names: true)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ require 'twitter_tweet_bot/client/api'
2
+ require 'twitter_tweet_bot/client/entity'
3
+
4
+ module TwitterTweetBot
5
+ class Client
6
+ include API
7
+ include Entity
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ end
12
+
13
+ private
14
+
15
+ attr_accessor :config
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ module TwitterTweetBot
2
+ class Configration
3
+ attr_accessor :name,
4
+ :client_id,
5
+ :client_secret,
6
+ :redirect_uri,
7
+ :scopes
8
+
9
+ def initialize(
10
+ name: nil,
11
+ client_id: nil,
12
+ client_secret: nil,
13
+ redirect_uri: nil,
14
+ scopes: []
15
+ )
16
+ @name = name
17
+ @client_id = client_id
18
+ @client_secret = client_secret
19
+ @redirect_uri = redirect_uri
20
+ @scopes = scopes
21
+
22
+ yield self if block_given?
23
+ end
24
+
25
+ def to_hash
26
+ instance_variables.reduce({}) do |hash, key|
27
+ hash[key[1..].to_sym] = instance_variable_get(key)
28
+ hash
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ require 'twitter_tweet_bot/entity/base'
2
+
3
+ module TwitterTweetBot
4
+ module Entity
5
+ class Authorization
6
+ include Base
7
+
8
+ act_as_entity(
9
+ :url,
10
+ :code_verifier,
11
+ :state
12
+ )
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/class/attribute'
3
+
4
+ module TwitterTweetBot
5
+ module Entity
6
+ module Base
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def act_as_entity(*fields)
11
+ class_attribute :fields,
12
+ instance_writer: false,
13
+ default: fields
14
+
15
+ attr_reader(*fields)
16
+ end
17
+ end
18
+
19
+ def initialize(json)
20
+ set_fields!(Hash(json))
21
+ end
22
+
23
+ private
24
+
25
+ def set_fields!(hash)
26
+ fields.each do |field|
27
+ instance_variable_set(:"@#{field}", hash[field])
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ require 'twitter_tweet_bot/entity/base'
2
+
3
+ module TwitterTweetBot
4
+ module Entity
5
+ class Token
6
+ include Base
7
+
8
+ act_as_entity(
9
+ :token_type,
10
+ :expires_in,
11
+ :access_token,
12
+ :refresh_token,
13
+ :scope
14
+ )
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ require 'twitter_tweet_bot/entity/base'
2
+
3
+ module TwitterTweetBot
4
+ module Entity
5
+ class Tweet
6
+ include Base
7
+
8
+ act_as_entity(
9
+ :id,
10
+ :text,
11
+ :edit_history_tweet_ids
12
+ )
13
+
14
+ def initialize(data)
15
+ super Hash(data)[:data]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+
2
+ require 'twitter_tweet_bot/entity/base'
3
+
4
+ module TwitterTweetBot
5
+ module Entity
6
+ class User
7
+ include Base
8
+
9
+ act_as_entity(
10
+ :id,
11
+ :name,
12
+ :username
13
+ )
14
+
15
+ def initialize(data)
16
+ super Hash(data)[:data]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,4 @@
1
+ require 'twitter_tweet_bot/entity/authorization'
2
+ require 'twitter_tweet_bot/entity/token'
3
+ require 'twitter_tweet_bot/entity/tweet'
4
+ require 'twitter_tweet_bot/entity/user'
@@ -0,0 +1,3 @@
1
+ module TwitterTweetBot
2
+ VERSION = '1.0.0'.freeze
3
+ end
@@ -0,0 +1,35 @@
1
+ require 'twitter_tweet_bot/client'
2
+ require 'twitter_tweet_bot/configration'
3
+ require 'twitter_tweet_bot/version'
4
+
5
+ require 'active_support/core_ext/module/attribute_accessors'
6
+ require 'active_support/core_ext/module/delegation'
7
+
8
+ module TwitterTweetBot
9
+ NoConfigrationError = Class.new(StandardError).freeze
10
+
11
+ mattr_accessor :default_config
12
+
13
+ class << self
14
+ def configure(&block)
15
+ self.default_config = Configration.new(&block)
16
+ end
17
+
18
+ def client(config = nil)
19
+ Client.new(config || default_config!)
20
+ end
21
+
22
+ delegate :authorization,
23
+ :fetch_token,
24
+ :refresh_token,
25
+ :post_tweet,
26
+ :users_me,
27
+ to: :client
28
+
29
+ private
30
+
31
+ def default_config!
32
+ default_config or raise NoConfigrationError
33
+ end
34
+ end
35
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: twitter_tweet_bot
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - SongCastle
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-05-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.0.3
27
+ description: Ruby implementation for Twitter client, using V2 API and OAuth 2.0.
28
+ email: "-"
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - README.md
34
+ - lib/twitter_tweet_bot.rb
35
+ - lib/twitter_tweet_bot/api.rb
36
+ - lib/twitter_tweet_bot/api/access_token.rb
37
+ - lib/twitter_tweet_bot/api/authorization.rb
38
+ - lib/twitter_tweet_bot/api/authorization/secure_code.rb
39
+ - lib/twitter_tweet_bot/api/http.rb
40
+ - lib/twitter_tweet_bot/api/http/base.rb
41
+ - lib/twitter_tweet_bot/api/http/error.rb
42
+ - lib/twitter_tweet_bot/api/http/get.rb
43
+ - lib/twitter_tweet_bot/api/http/headers.rb
44
+ - lib/twitter_tweet_bot/api/http/post.rb
45
+ - lib/twitter_tweet_bot/api/refresh_token.rb
46
+ - lib/twitter_tweet_bot/api/tweet.rb
47
+ - lib/twitter_tweet_bot/api/users_me.rb
48
+ - lib/twitter_tweet_bot/cache.rb
49
+ - lib/twitter_tweet_bot/cache/caching.rb
50
+ - lib/twitter_tweet_bot/cache/client_ext.rb
51
+ - lib/twitter_tweet_bot/cache/configuration_ext.rb
52
+ - lib/twitter_tweet_bot/cache/entity_ext.rb
53
+ - lib/twitter_tweet_bot/cache/entity_ext/authorization.rb
54
+ - lib/twitter_tweet_bot/cache/entity_ext/base.rb
55
+ - lib/twitter_tweet_bot/cache/entity_ext/token.rb
56
+ - lib/twitter_tweet_bot/cache/store.rb
57
+ - lib/twitter_tweet_bot/client.rb
58
+ - lib/twitter_tweet_bot/client/api.rb
59
+ - lib/twitter_tweet_bot/client/entity.rb
60
+ - lib/twitter_tweet_bot/configration.rb
61
+ - lib/twitter_tweet_bot/entity.rb
62
+ - lib/twitter_tweet_bot/entity/authorization.rb
63
+ - lib/twitter_tweet_bot/entity/base.rb
64
+ - lib/twitter_tweet_bot/entity/token.rb
65
+ - lib/twitter_tweet_bot/entity/tweet.rb
66
+ - lib/twitter_tweet_bot/entity/user.rb
67
+ - lib/twitter_tweet_bot/version.rb
68
+ homepage: https://github.com/SongCastle/twitter_tweet_bot
69
+ licenses:
70
+ - MIT
71
+ metadata: {}
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '3.0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubygems_version: 3.0.3
88
+ signing_key:
89
+ specification_version: 4
90
+ summary: Ruby implementation for Twitter twetting client
91
+ test_files: []