slack-ruby-client 0.13.1 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/.rubocop_todo.yml +9 -32
- data/.travis.yml +4 -4
- data/CHANGELOG.md +10 -0
- data/Dangerfile +1 -0
- data/Gemfile +1 -3
- data/LICENSE.md +1 -1
- data/README.md +100 -13
- data/bin/commands.rb +1 -0
- data/bin/commands/apps.rb +14 -0
- data/bin/commands/chat.rb +5 -1
- data/bin/commands/conversations.rb +1 -0
- data/bin/commands/files.rb +8 -9
- data/bin/commands/reactions.rb +2 -2
- data/bin/slack +1 -1
- data/lib/slack-ruby-client.rb +4 -0
- data/lib/slack/events/config.rb +31 -0
- data/lib/slack/events/request.rb +60 -0
- data/lib/slack/real_time/client.rb +35 -7
- data/lib/slack/real_time/concurrency/async.rb +34 -2
- data/lib/slack/real_time/concurrency/celluloid.rb +28 -9
- data/lib/slack/real_time/concurrency/eventmachine.rb +25 -4
- data/lib/slack/real_time/socket.rb +19 -0
- data/lib/slack/real_time/stores/store.rb +2 -0
- data/lib/slack/version.rb +1 -1
- data/lib/slack/web/api/endpoints.rb +2 -0
- data/lib/slack/web/api/endpoints/apps.rb +26 -0
- data/lib/slack/web/api/endpoints/chat.rb +30 -4
- data/lib/slack/web/api/endpoints/conversations.rb +2 -0
- data/lib/slack/web/api/endpoints/files.rb +8 -9
- data/lib/slack/web/api/endpoints/reactions.rb +2 -2
- data/lib/slack/web/api/patches/chat.6.block-kit-support.patch +69 -0
- data/lib/slack/web/pagination/cursor.rb +3 -0
- data/lib/tasks/real_time.rake +2 -0
- data/lib/tasks/web.rake +1 -0
- data/slack-ruby-client.gemspec +3 -2
- data/spec/integration/integration_spec.rb +64 -6
- data/spec/slack/events/config_spec.rb +29 -0
- data/spec/slack/events/request_spec.rb +121 -0
- data/spec/slack/real_time/client_spec.rb +36 -1
- data/spec/slack/real_time/concurrency/eventmachine_spec.rb +1 -0
- data/spec/slack/web/api/endpoints/apps_spec.rb +15 -0
- data/spec/slack/web/api/endpoints/custom_specs/chat_spec.rb +45 -24
- data/spec/spec_helper.rb +1 -0
- data/spec/support/queue_with_timeout.rb +4 -4
- metadata +29 -4
@@ -0,0 +1,60 @@
|
|
1
|
+
module Slack
|
2
|
+
module Events
|
3
|
+
class Request
|
4
|
+
class MissingSigningSecret < StandardError; end
|
5
|
+
class TimestampExpired < StandardError; end
|
6
|
+
class InvalidSignature < StandardError; end
|
7
|
+
|
8
|
+
attr_reader :http_request
|
9
|
+
|
10
|
+
def initialize(http_request)
|
11
|
+
@http_request = http_request
|
12
|
+
end
|
13
|
+
|
14
|
+
# Request timestamp.
|
15
|
+
def timestamp
|
16
|
+
@timestamp ||= http_request.headers['X-Slack-Request-Timestamp']
|
17
|
+
end
|
18
|
+
|
19
|
+
# The signature is created by combining the signing secret with the body of the request
|
20
|
+
# Slack is sending using a standard HMAC-SHA256 keyed hash.
|
21
|
+
def signature
|
22
|
+
@signature ||= http_request.headers['X-Slack-Signature']
|
23
|
+
end
|
24
|
+
|
25
|
+
# Signature version.
|
26
|
+
def version
|
27
|
+
'v0'
|
28
|
+
end
|
29
|
+
|
30
|
+
# Request body.
|
31
|
+
def body
|
32
|
+
@body ||= http_request.body.read
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns true if the signature coming from Slack has expired.
|
36
|
+
def expired?
|
37
|
+
timestamp.nil? || (Time.now.to_i - timestamp.to_i).abs > Slack::Events.config.signature_expires_in
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true if the signature coming from Slack is valid.
|
41
|
+
def valid?
|
42
|
+
raise MissingSigningSecret unless Slack::Events.config.signing_secret
|
43
|
+
|
44
|
+
digest = OpenSSL::Digest::SHA256.new
|
45
|
+
signature_basestring = [version, timestamp, body].join(':')
|
46
|
+
hex_hash = OpenSSL::HMAC.hexdigest(digest, Slack::Events.config.signing_secret, signature_basestring)
|
47
|
+
computed_signature = [version, hex_hash].join('=')
|
48
|
+
computed_signature == signature
|
49
|
+
end
|
50
|
+
|
51
|
+
# Validates the request signature and its expiration.
|
52
|
+
def verify!
|
53
|
+
raise TimestampExpired if expired?
|
54
|
+
raise InvalidSignature unless valid?
|
55
|
+
|
56
|
+
true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -47,7 +47,7 @@ module Slack
|
|
47
47
|
# Start RealTime client and block until it disconnects.
|
48
48
|
def start!(&block)
|
49
49
|
@callback = block if block_given?
|
50
|
-
|
50
|
+
build_socket
|
51
51
|
@socket.start_sync(self)
|
52
52
|
end
|
53
53
|
|
@@ -55,12 +55,13 @@ module Slack
|
|
55
55
|
# The RealTime::Client will run in the background.
|
56
56
|
def start_async(&block)
|
57
57
|
@callback = block if block_given?
|
58
|
-
|
58
|
+
build_socket
|
59
59
|
@socket.start_async(self)
|
60
60
|
end
|
61
61
|
|
62
62
|
def stop!
|
63
63
|
raise ClientNotStartedError unless started?
|
64
|
+
|
64
65
|
@socket.disconnect! if @socket
|
65
66
|
end
|
66
67
|
|
@@ -102,16 +103,41 @@ module Slack
|
|
102
103
|
end
|
103
104
|
end
|
104
105
|
|
106
|
+
def run_ping!
|
107
|
+
time_since_last_message = @socket.time_since_last_message
|
108
|
+
return if time_since_last_message < websocket_ping
|
109
|
+
raise Slack::RealTime::Client::ClientNotStartedError if !@socket.connected? || time_since_last_message > (websocket_ping * 2)
|
110
|
+
|
111
|
+
ping
|
112
|
+
rescue Slack::RealTime::Client::ClientNotStartedError
|
113
|
+
restart_async
|
114
|
+
end
|
115
|
+
|
116
|
+
def run_ping?
|
117
|
+
!websocket_ping.nil? && websocket_ping > 0
|
118
|
+
end
|
119
|
+
|
105
120
|
protected
|
106
121
|
|
122
|
+
def restart_async
|
123
|
+
logger.debug("#{self.class}##{__method__}")
|
124
|
+
@socket.close
|
125
|
+
start = web_client.send(rtm_start_method, start_options)
|
126
|
+
data = Slack::Messages::Message.new(start)
|
127
|
+
@url = data.url
|
128
|
+
@store = @store_class.new(data) if @store_class
|
129
|
+
@socket.restart_async(self, @url)
|
130
|
+
end
|
131
|
+
|
107
132
|
# @return [Slack::RealTime::Socket]
|
108
133
|
def build_socket
|
109
134
|
raise ClientAlreadyStartedError if started?
|
135
|
+
|
110
136
|
start = web_client.send(rtm_start_method, start_options)
|
111
137
|
data = Slack::Messages::Message.new(start)
|
112
138
|
@url = data.url
|
113
139
|
@store = @store_class.new(data) if @store_class
|
114
|
-
socket_class.new(@url, socket_options)
|
140
|
+
@socket = socket_class.new(@url, socket_options)
|
115
141
|
end
|
116
142
|
|
117
143
|
def rtm_start_method
|
@@ -139,6 +165,7 @@ module Slack
|
|
139
165
|
|
140
166
|
def send_json(data)
|
141
167
|
raise ClientNotStartedError unless started?
|
168
|
+
|
142
169
|
logger.debug("#{self.class}##{__method__}") { data }
|
143
170
|
@socket.send_data(data.to_json)
|
144
171
|
end
|
@@ -146,10 +173,7 @@ module Slack
|
|
146
173
|
def open(_event); end
|
147
174
|
|
148
175
|
def close(_event)
|
149
|
-
socket
|
150
|
-
@socket = nil
|
151
|
-
|
152
|
-
[socket, socket_class].each do |s|
|
176
|
+
[@socket, socket_class].each do |s|
|
153
177
|
s.close if s.respond_to?(:close)
|
154
178
|
end
|
155
179
|
end
|
@@ -157,6 +181,7 @@ module Slack
|
|
157
181
|
def callback(event, type)
|
158
182
|
callbacks = self.callbacks[type.to_s]
|
159
183
|
return false unless callbacks
|
184
|
+
|
160
185
|
callbacks.each do |c|
|
161
186
|
c.call(event)
|
162
187
|
end
|
@@ -168,9 +193,11 @@ module Slack
|
|
168
193
|
|
169
194
|
def dispatch(event)
|
170
195
|
return false unless event.data
|
196
|
+
|
171
197
|
data = Slack::Messages::Message.new(JSON.parse(event.data))
|
172
198
|
type = data.type
|
173
199
|
return false unless type
|
200
|
+
|
174
201
|
type = type.to_s
|
175
202
|
logger.debug("#{self.class}##{__method__}") { data.to_s }
|
176
203
|
run_handlers(type, data) if @store
|
@@ -195,6 +222,7 @@ module Slack
|
|
195
222
|
def run_callbacks(type, data)
|
196
223
|
callbacks = self.callbacks[type]
|
197
224
|
return false unless callbacks
|
225
|
+
|
198
226
|
callbacks.each do |c|
|
199
227
|
c.call(data)
|
200
228
|
end
|
@@ -1,9 +1,14 @@
|
|
1
1
|
require 'async/websocket'
|
2
|
+
require 'async/clock'
|
2
3
|
|
3
4
|
module Slack
|
4
5
|
module RealTime
|
5
6
|
module Concurrency
|
6
7
|
module Async
|
8
|
+
class Reactor < ::Async::Reactor
|
9
|
+
def_delegators :@timers, :cancel
|
10
|
+
end
|
11
|
+
|
7
12
|
class Client < ::Async::WebSocket::Client
|
8
13
|
extend ::Forwardable
|
9
14
|
def_delegators :@driver, :on, :text, :binary, :emit
|
@@ -13,13 +18,40 @@ module Slack
|
|
13
18
|
attr_reader :client
|
14
19
|
|
15
20
|
def start_async(client)
|
21
|
+
@reactor = Reactor.new
|
16
22
|
Thread.new do
|
17
|
-
|
18
|
-
client.
|
23
|
+
if client.run_ping?
|
24
|
+
@reactor.every(client.websocket_ping) do
|
25
|
+
client.run_ping!
|
26
|
+
end
|
27
|
+
end
|
28
|
+
@reactor.run do |task|
|
29
|
+
task.async do
|
30
|
+
client.run_loop
|
31
|
+
end
|
19
32
|
end
|
20
33
|
end
|
21
34
|
end
|
22
35
|
|
36
|
+
def restart_async(client, new_url)
|
37
|
+
@url = new_url
|
38
|
+
@last_message_at = current_time
|
39
|
+
return unless @reactor
|
40
|
+
|
41
|
+
@reactor.async do
|
42
|
+
client.run_loop
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def disconnect!
|
47
|
+
super
|
48
|
+
@reactor.cancel
|
49
|
+
end
|
50
|
+
|
51
|
+
def current_time
|
52
|
+
::Async::Clock.now
|
53
|
+
end
|
54
|
+
|
23
55
|
def connect!
|
24
56
|
super
|
25
57
|
run_loop
|
@@ -37,29 +37,29 @@ module Slack
|
|
37
37
|
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE => e
|
38
38
|
logger.debug("#{self.class}##{__method__}") { e }
|
39
39
|
driver.emit(:close, WebSocket::Driver::CloseEvent.new(1001, 'server closed connection')) unless @closing
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def disconnect!
|
43
|
+
super
|
44
|
+
@ping_timer.cancel if @ping_timer
|
46
45
|
end
|
47
46
|
|
48
47
|
def close
|
49
48
|
@closing = true
|
50
|
-
driver.close
|
49
|
+
driver.close if driver
|
51
50
|
super
|
52
51
|
end
|
53
52
|
|
54
53
|
def read
|
55
54
|
buffer = socket.readpartial(BLOCK_SIZE)
|
56
55
|
raise EOFError unless buffer && !buffer.empty?
|
56
|
+
|
57
57
|
async.handle_read(buffer)
|
58
58
|
end
|
59
59
|
|
60
60
|
def handle_read(buffer)
|
61
61
|
logger.debug("#{self.class}##{__method__}") { buffer }
|
62
|
-
driver.parse buffer
|
62
|
+
driver.parse buffer if driver
|
63
63
|
end
|
64
64
|
|
65
65
|
def write(data)
|
@@ -70,14 +70,33 @@ module Slack
|
|
70
70
|
def start_async(client)
|
71
71
|
@client = client
|
72
72
|
Actor.new(future.run_client_loop)
|
73
|
+
Actor.new(future.run_ping_loop)
|
73
74
|
end
|
74
75
|
|
75
76
|
def run_client_loop
|
76
77
|
@client.run_loop
|
78
|
+
rescue StandardError => e
|
79
|
+
logger.debug("#{self.class}##{__method__}") { e }
|
80
|
+
raise e
|
81
|
+
end
|
82
|
+
|
83
|
+
def run_ping_loop
|
84
|
+
return unless @client.run_ping?
|
85
|
+
|
86
|
+
@ping_timer = every @client.websocket_ping do
|
87
|
+
@client.run_ping!
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def restart_async(client, new_url)
|
92
|
+
@last_message_at = current_time
|
93
|
+
@url = new_url
|
94
|
+
@client = client
|
95
|
+
Actor.new(future.run_client_loop)
|
77
96
|
end
|
78
97
|
|
79
98
|
def connected?
|
80
|
-
!@connected.nil?
|
99
|
+
!@connected.nil? && !@driver.nil?
|
81
100
|
end
|
82
101
|
|
83
102
|
protected
|
@@ -10,8 +10,8 @@ module Slack
|
|
10
10
|
protected :logger
|
11
11
|
|
12
12
|
def initialize(url, protocols = nil, options = {})
|
13
|
-
@logger = options.
|
14
|
-
super
|
13
|
+
@logger = options.fetch(:logger) || Slack::RealTime::Config.logger || Slack::Config.logger
|
14
|
+
super url, protocols, options.except(:logger)
|
15
15
|
end
|
16
16
|
|
17
17
|
def parse(data)
|
@@ -29,17 +29,38 @@ module Slack
|
|
29
29
|
def start_async(client)
|
30
30
|
@thread = ensure_reactor_running
|
31
31
|
|
32
|
+
if client.run_ping?
|
33
|
+
EventMachine.add_periodic_timer(client.websocket_ping) do
|
34
|
+
client.run_ping!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
32
38
|
client.run_loop
|
33
39
|
|
34
40
|
@thread
|
35
41
|
end
|
36
42
|
|
37
|
-
def
|
43
|
+
def restart_async(client, new_url)
|
44
|
+
@url = new_url
|
45
|
+
@last_message_at = current_time
|
46
|
+
@thread = ensure_reactor_running
|
47
|
+
|
48
|
+
client.run_loop
|
49
|
+
|
50
|
+
@thread
|
51
|
+
end
|
52
|
+
|
53
|
+
def disconnect!
|
38
54
|
super
|
39
|
-
EventMachine.
|
55
|
+
EventMachine.stop_event_loop if EventMachine.reactor_running?
|
40
56
|
@thread = nil
|
41
57
|
end
|
42
58
|
|
59
|
+
def close
|
60
|
+
driver.close if driver
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
43
64
|
def send_data(message)
|
44
65
|
logger.debug("#{self.class}##{__method__}") { message }
|
45
66
|
driver.send(message)
|
@@ -12,6 +12,7 @@ module Slack
|
|
12
12
|
@options = options
|
13
13
|
@driver = nil
|
14
14
|
@logger = options.delete(:logger) || Slack::RealTime::Config.logger || Slack::Config.logger
|
15
|
+
@last_message_at = nil
|
15
16
|
end
|
16
17
|
|
17
18
|
def send_data(message)
|
@@ -30,6 +31,10 @@ module Slack
|
|
30
31
|
connect
|
31
32
|
logger.debug("#{self.class}##{__method__}") { driver.class }
|
32
33
|
|
34
|
+
driver.on :message do
|
35
|
+
@last_message_at = current_time
|
36
|
+
end
|
37
|
+
|
33
38
|
yield driver if block_given?
|
34
39
|
end
|
35
40
|
|
@@ -53,6 +58,20 @@ module Slack
|
|
53
58
|
raise NotImplementedError, "Expected #{self.class} to implement #{__method__}."
|
54
59
|
end
|
55
60
|
|
61
|
+
def restart_async(_client, _url)
|
62
|
+
raise NotImplementedError, "Expected #{self.class} to implement #{__method__}."
|
63
|
+
end
|
64
|
+
|
65
|
+
def time_since_last_message
|
66
|
+
return 0 unless @last_message_at
|
67
|
+
|
68
|
+
current_time - @last_message_at
|
69
|
+
end
|
70
|
+
|
71
|
+
def current_time
|
72
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
73
|
+
end
|
74
|
+
|
56
75
|
def close
|
57
76
|
@driver = nil
|
58
77
|
end
|
@@ -369,6 +369,7 @@ module Slack
|
|
369
369
|
# @see https://github.com/slack-ruby/slack-api-ref/blob/master/events/im_close.json
|
370
370
|
on :im_close do |data|
|
371
371
|
return unless ims && ims.key?(data.channel)
|
372
|
+
|
372
373
|
ims[data.channel].is_open = false
|
373
374
|
end
|
374
375
|
|
@@ -394,6 +395,7 @@ module Slack
|
|
394
395
|
# @see https://github.com/slack-ruby/slack-api-ref/blob/master/events/im_open.json
|
395
396
|
on :im_open do |data|
|
396
397
|
return unless ims && ims.key?(data.channel)
|
398
|
+
|
397
399
|
ims[data.channel].is_open = true
|
398
400
|
end
|
399
401
|
|
data/lib/slack/version.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# This file was auto-generated by lib/tasks/web.rake
|
2
2
|
|
3
3
|
require_relative 'endpoints/api'
|
4
|
+
require_relative 'endpoints/apps'
|
4
5
|
require_relative 'endpoints/apps_permissions'
|
5
6
|
require_relative 'endpoints/apps_permissions_resources'
|
6
7
|
require_relative 'endpoints/apps_permissions_scopes'
|
@@ -44,6 +45,7 @@ module Slack
|
|
44
45
|
include Slack::Web::Api::Mixins::Groups
|
45
46
|
|
46
47
|
include Api
|
48
|
+
include Apps
|
47
49
|
include AppsPermissions
|
48
50
|
include AppsPermissionsResources
|
49
51
|
include AppsPermissionsScopes
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# This file was auto-generated by lib/tasks/web.rake
|
2
|
+
|
3
|
+
module Slack
|
4
|
+
module Web
|
5
|
+
module Api
|
6
|
+
module Endpoints
|
7
|
+
module Apps
|
8
|
+
#
|
9
|
+
# Uninstalls your app from a workspace.
|
10
|
+
#
|
11
|
+
# @option options [Object] :client_id
|
12
|
+
# Issued when you created your application.
|
13
|
+
# @option options [Object] :client_secret
|
14
|
+
# Issued when you created your application.
|
15
|
+
# @see https://api.slack.com/methods/apps.uninstall
|
16
|
+
# @see https://github.com/slack-ruby/slack-api-ref/blob/master/methods/apps/apps.uninstall.json
|
17
|
+
def apps_uninstall(options = {})
|
18
|
+
throw ArgumentError.new('Required arguments :client_id missing') if options[:client_id].nil?
|
19
|
+
throw ArgumentError.new('Required arguments :client_secret missing') if options[:client_secret].nil?
|
20
|
+
post('apps.uninstall', options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|