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.
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