twitter_tweet_bot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []