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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.rubocop_todo.yml +9 -32
  4. data/.travis.yml +4 -4
  5. data/CHANGELOG.md +10 -0
  6. data/Dangerfile +1 -0
  7. data/Gemfile +1 -3
  8. data/LICENSE.md +1 -1
  9. data/README.md +100 -13
  10. data/bin/commands.rb +1 -0
  11. data/bin/commands/apps.rb +14 -0
  12. data/bin/commands/chat.rb +5 -1
  13. data/bin/commands/conversations.rb +1 -0
  14. data/bin/commands/files.rb +8 -9
  15. data/bin/commands/reactions.rb +2 -2
  16. data/bin/slack +1 -1
  17. data/lib/slack-ruby-client.rb +4 -0
  18. data/lib/slack/events/config.rb +31 -0
  19. data/lib/slack/events/request.rb +60 -0
  20. data/lib/slack/real_time/client.rb +35 -7
  21. data/lib/slack/real_time/concurrency/async.rb +34 -2
  22. data/lib/slack/real_time/concurrency/celluloid.rb +28 -9
  23. data/lib/slack/real_time/concurrency/eventmachine.rb +25 -4
  24. data/lib/slack/real_time/socket.rb +19 -0
  25. data/lib/slack/real_time/stores/store.rb +2 -0
  26. data/lib/slack/version.rb +1 -1
  27. data/lib/slack/web/api/endpoints.rb +2 -0
  28. data/lib/slack/web/api/endpoints/apps.rb +26 -0
  29. data/lib/slack/web/api/endpoints/chat.rb +30 -4
  30. data/lib/slack/web/api/endpoints/conversations.rb +2 -0
  31. data/lib/slack/web/api/endpoints/files.rb +8 -9
  32. data/lib/slack/web/api/endpoints/reactions.rb +2 -2
  33. data/lib/slack/web/api/patches/chat.6.block-kit-support.patch +69 -0
  34. data/lib/slack/web/pagination/cursor.rb +3 -0
  35. data/lib/tasks/real_time.rake +2 -0
  36. data/lib/tasks/web.rake +1 -0
  37. data/slack-ruby-client.gemspec +3 -2
  38. data/spec/integration/integration_spec.rb +64 -6
  39. data/spec/slack/events/config_spec.rb +29 -0
  40. data/spec/slack/events/request_spec.rb +121 -0
  41. data/spec/slack/real_time/client_spec.rb +36 -1
  42. data/spec/slack/real_time/concurrency/eventmachine_spec.rb +1 -0
  43. data/spec/slack/web/api/endpoints/apps_spec.rb +15 -0
  44. data/spec/slack/web/api/endpoints/custom_specs/chat_spec.rb +45 -24
  45. data/spec/spec_helper.rb +1 -0
  46. data/spec/support/queue_with_timeout.rb +4 -4
  47. 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
- @socket = build_socket
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
- @socket = build_socket
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 = @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
- ::Async::Reactor.run do
18
- client.run_loop
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
- ensure
41
- begin
42
- current_actor.terminate if current_actor.alive?
43
- rescue StandardError
44
- nil
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.delete(:logger) || Slack::RealTime::Config.logger || Slack::Config.logger
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 close
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.stop if @thread
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
 
@@ -1,3 +1,3 @@
1
1
  module Slack
2
- VERSION = '0.13.1'.freeze
2
+ VERSION = '0.14.0'.freeze
3
3
  end
@@ -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