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.
- checksums.yaml +7 -0
- data/README.md +172 -0
- data/lib/twitter_tweet_bot/api/access_token.rb +64 -0
- data/lib/twitter_tweet_bot/api/authorization/secure_code.rb +77 -0
- data/lib/twitter_tweet_bot/api/authorization.rb +71 -0
- data/lib/twitter_tweet_bot/api/http/base.rb +17 -0
- data/lib/twitter_tweet_bot/api/http/error.rb +24 -0
- data/lib/twitter_tweet_bot/api/http/get.rb +28 -0
- data/lib/twitter_tweet_bot/api/http/headers.rb +21 -0
- data/lib/twitter_tweet_bot/api/http/post.rb +46 -0
- data/lib/twitter_tweet_bot/api/http.rb +31 -0
- data/lib/twitter_tweet_bot/api/refresh_token.rb +55 -0
- data/lib/twitter_tweet_bot/api/tweet.rb +36 -0
- data/lib/twitter_tweet_bot/api/users_me.rb +36 -0
- data/lib/twitter_tweet_bot/api.rb +5 -0
- data/lib/twitter_tweet_bot/cache/caching.rb +32 -0
- data/lib/twitter_tweet_bot/cache/client_ext.rb +29 -0
- data/lib/twitter_tweet_bot/cache/configuration_ext.rb +18 -0
- data/lib/twitter_tweet_bot/cache/entity_ext/authorization.rb +16 -0
- data/lib/twitter_tweet_bot/cache/entity_ext/base.rb +31 -0
- data/lib/twitter_tweet_bot/cache/entity_ext/token.rb +16 -0
- data/lib/twitter_tweet_bot/cache/entity_ext.rb +2 -0
- data/lib/twitter_tweet_bot/cache/store.rb +41 -0
- data/lib/twitter_tweet_bot/cache.rb +13 -0
- data/lib/twitter_tweet_bot/client/api.rb +50 -0
- data/lib/twitter_tweet_bot/client/entity.rb +38 -0
- data/lib/twitter_tweet_bot/client.rb +17 -0
- data/lib/twitter_tweet_bot/configration.rb +32 -0
- data/lib/twitter_tweet_bot/entity/authorization.rb +15 -0
- data/lib/twitter_tweet_bot/entity/base.rb +32 -0
- data/lib/twitter_tweet_bot/entity/token.rb +17 -0
- data/lib/twitter_tweet_bot/entity/tweet.rb +19 -0
- data/lib/twitter_tweet_bot/entity/user.rb +20 -0
- data/lib/twitter_tweet_bot/entity.rb +4 -0
- data/lib/twitter_tweet_bot/version.rb +3 -0
- data/lib/twitter_tweet_bot.rb +35 -0
- 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,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,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,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,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,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,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: []
|