slack-ruby-client 0.13.1 → 0.14.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 +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
|