slack_socket_mode_bot 0.9.2 → 0.9.3
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/README.md +72 -5
- data/lib/slack_socket_mode_bot/simple_web_socket.rb +30 -17
- data/lib/slack_socket_mode_bot/test_mode.rb +49 -0
- data/lib/slack_socket_mode_bot/version.rb +1 -1
- data/lib/slack_socket_mode_bot.rb +82 -56
- data/sig/{slack_socket_mode.rbs → slack_socket_mode_bot.rbs} +12 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a296df6a07779baa7df57823973e3d928a82859b4cc325066b7d2ece2a6a66e9
|
|
4
|
+
data.tar.gz: f6403d418f85da1a14b3f2c696d93bb8c09f67723e096038445b424e3460d249
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 441c1d5466ec9bdd9032e6761e49fd18cf56764e4b5409e021a1d6b174d8d2b7ea9b70e59f2d6aa659185146c2b49f8568ec09890c3d234b0d5107a107598efd
|
|
7
|
+
data.tar.gz: 0b8115dbaf7727617df6b440e824496116146850f6d554db454c23426cadec48b4efc77a0f3b72fece57fc6850f3ea7224c603b16546f17817857dcd6decea97
|
data/README.md
CHANGED
|
@@ -17,11 +17,11 @@ require "logger"
|
|
|
17
17
|
|
|
18
18
|
# Slack's Bot User OAuth Token
|
|
19
19
|
# You can create this token with: https://api.slack.com/apps/ - "OAuth & Permissions" - "OAuth Tokens for Your Workspace"
|
|
20
|
-
SLACK_BOT_TOKEN = "xoxb-..."
|
|
20
|
+
SLACK_BOT_TOKEN = ENV.fetch("SLACK_BOT_TOKEN") # "xoxb-...", or "test" for test mode
|
|
21
21
|
|
|
22
22
|
# Slack's App-Level Token
|
|
23
23
|
# You can create this token with: https://api.slack.com/apps/ - "Basic Information" - "App-Level Tokens"
|
|
24
|
-
SLACK_APP_TOKEN = "xapp-..."
|
|
24
|
+
SLACK_APP_TOKEN = ENV.fetch("SLACK_APP_TOKEN") # "xapp-..."
|
|
25
25
|
|
|
26
26
|
logger = Logger.new(STDOUT, level: Logger::Severity::INFO)
|
|
27
27
|
|
|
@@ -60,11 +60,39 @@ rescue Exception
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# Start the communication
|
|
63
|
-
bot.run
|
|
63
|
+
bot.run unless bot.test_mode?
|
|
64
|
+
|
|
65
|
+
# Test
|
|
66
|
+
if bot.test_mode?
|
|
67
|
+
# Stub chat.postMessage
|
|
68
|
+
posted = []
|
|
69
|
+
bot.test_mode.stub_web_api("chat.postMessage") {|data| posted << data }
|
|
70
|
+
|
|
71
|
+
# Simulate receiving an event from Slack
|
|
72
|
+
data = {
|
|
73
|
+
type: "events_api",
|
|
74
|
+
payload: {
|
|
75
|
+
event: {
|
|
76
|
+
type: "app_mention",
|
|
77
|
+
text: "hello",
|
|
78
|
+
channel: "C123",
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
bot.test_mode.simulate_envelope(data)
|
|
83
|
+
|
|
84
|
+
# OK if chat.postMessage was sent
|
|
85
|
+
ok = posted == [{ channel: "C123", text: "echo:hello" }]
|
|
86
|
+
|
|
87
|
+
puts "test: #{ ok ? "OK" : "NG" }"
|
|
88
|
+
end
|
|
64
89
|
```
|
|
65
90
|
|
|
66
91
|
```
|
|
67
|
-
$ ruby examples/echo_bot.rb
|
|
92
|
+
$ SLACK_BOT_TOKEN=test SLACK_APP_TOKEN=test ruby examples/echo_bot.rb
|
|
93
|
+
test: OK
|
|
94
|
+
|
|
95
|
+
$ SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-... ruby examples/echo_bot.rb
|
|
68
96
|
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2560] websocket open
|
|
69
97
|
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2600] websocket open
|
|
70
98
|
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2640] websocket open
|
|
@@ -85,7 +113,7 @@ I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] events_api app_ment
|
|
|
85
113
|
|
|
86
114
|
Connects to Slack with Socket Mode.
|
|
87
115
|
|
|
88
|
-
* `token`: Slack's Bot User OAuth token (starting with `xoxb-`)
|
|
116
|
+
* `token`: Slack's Bot User OAuth token (starting with `xoxb-`), or `"test"` to build a test-mode bot (see below)
|
|
89
117
|
* `app_token`: Slack's App-Level token (starting with `xapp-`)
|
|
90
118
|
* `logger`: A Logger instance (optional)
|
|
91
119
|
* block: Handles events received from Slack
|
|
@@ -129,3 +157,42 @@ end
|
|
|
129
157
|
|
|
130
158
|
This method allows you to manage the main loop yourself.
|
|
131
159
|
If you don't need it, you can just use `SlackSocketModeBot#run`.
|
|
160
|
+
|
|
161
|
+
## Test mode
|
|
162
|
+
|
|
163
|
+
Passing `token: "test"` (`SlackSocketModeBot.new(token: "test")`) puts the bot in test mode.
|
|
164
|
+
In this mode, the bot never connects to Slack.
|
|
165
|
+
Instead of opening a socket, you feed it events and stub the Web API yourself.
|
|
166
|
+
So you can test your handler offline.
|
|
167
|
+
|
|
168
|
+
See `examples/test_echo_bot.rb` for complete examples.
|
|
169
|
+
|
|
170
|
+
### `SlackSocketModeBot#test_mode?`
|
|
171
|
+
|
|
172
|
+
Returns whether the bot is in test mode, i.e. whether it was built with `token: "test"`.
|
|
173
|
+
|
|
174
|
+
### `SlackSocketModeBot#test_mode`
|
|
175
|
+
|
|
176
|
+
Returns the bot's `SlackSocketModeBot::TestMode` instance in test mode, or `nil` otherwise.
|
|
177
|
+
All of the methods below are called on it (e.g. `bot.test_mode.simulate_envelope(...)`).
|
|
178
|
+
|
|
179
|
+
### `SlackSocketModeBot::TestMode#stub_web_api(method, &block)`
|
|
180
|
+
|
|
181
|
+
Stubs a Web API method's response.
|
|
182
|
+
`bot.call(method, data)` then runs the block instead of making a real HTTP request.
|
|
183
|
+
|
|
184
|
+
An unstubbed call returns `{ ok: true }`.
|
|
185
|
+
When `$VERBOSE` is set, it also prints a warning.
|
|
186
|
+
|
|
187
|
+
### `SlackSocketModeBot::TestMode#simulate_envelope(envelope)`
|
|
188
|
+
|
|
189
|
+
Delivers an envelope to the bot as if it had arrived over the socket from Slack.
|
|
190
|
+
You can use it to run and test your event handler synchronously.
|
|
191
|
+
|
|
192
|
+
`envelope` is a Hash (string or symbol keys) shaped like the JSON Slack sends.
|
|
193
|
+
`:envelope_id` is filled in automatically if omitted.
|
|
194
|
+
|
|
195
|
+
### `SlackSocketModeBot::TestMode#reset`
|
|
196
|
+
|
|
197
|
+
Clears all registered stubs and the event-deduplication state.
|
|
198
|
+
Call it between test cases (for example, `setup` or `before` hooks).
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
class SlackSocketModeBot::SimpleWebSocket
|
|
2
|
+
# Seconds to wait for the server's TCP close before dropping the socket ourselves.
|
|
3
|
+
CLOSE_TIMEOUT = 10
|
|
4
|
+
|
|
2
5
|
def initialize(url)
|
|
3
6
|
uri = URI.parse(url)
|
|
4
7
|
|
|
@@ -25,9 +28,9 @@ class SlackSocketModeBot::SimpleWebSocket
|
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
@version = nil
|
|
31
|
+
@close_deadline = nil # nil while open; a Time once closing
|
|
28
32
|
|
|
29
33
|
@fib = Fiber.new do
|
|
30
|
-
closed = false
|
|
31
34
|
begin
|
|
32
35
|
handshake = WebSocket::Handshake::Client.new(url: url)
|
|
33
36
|
@write_buff = handshake.to_s.dup
|
|
@@ -42,8 +45,9 @@ class SlackSocketModeBot::SimpleWebSocket
|
|
|
42
45
|
while msg = frame.next
|
|
43
46
|
case msg.type
|
|
44
47
|
when :close
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
was_closing = closing?
|
|
49
|
+
close
|
|
50
|
+
yield :close unless was_closing
|
|
47
51
|
when :ping
|
|
48
52
|
send(msg.data, type: :pong)
|
|
49
53
|
when :pong
|
|
@@ -57,14 +61,15 @@ class SlackSocketModeBot::SimpleWebSocket
|
|
|
57
61
|
end
|
|
58
62
|
rescue EOFError
|
|
59
63
|
ensure
|
|
60
|
-
yield :close unless
|
|
61
|
-
@io.close
|
|
64
|
+
yield :close unless closing?
|
|
62
65
|
end
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
@fib.resume
|
|
66
69
|
end
|
|
67
70
|
|
|
71
|
+
def closing? = !@close_deadline.nil?
|
|
72
|
+
|
|
68
73
|
def send(data, type: :text, code: nil)
|
|
69
74
|
raise "not opened yet" unless @version
|
|
70
75
|
frame = WebSocket::Frame::Outgoing::Client.new(version: @version, data: data, type: type, code: code)
|
|
@@ -72,22 +77,19 @@ class SlackSocketModeBot::SimpleWebSocket
|
|
|
72
77
|
end
|
|
73
78
|
|
|
74
79
|
def close(code: 1000, reason: "")
|
|
75
|
-
|
|
80
|
+
return if closing? || @io.closed?
|
|
81
|
+
send(reason, type: :close, code: code)
|
|
82
|
+
@close_deadline = Time.now + CLOSE_TIMEOUT
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
def step(read_ios, write_ios)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
len = @io.write_nonblock(@write_buff, exception: false)
|
|
83
|
-
case len
|
|
84
|
-
when :wait_readable then wait_readable = true
|
|
85
|
-
when :wait_writable then wait_writable = true
|
|
86
|
-
else
|
|
87
|
-
@write_buff.clear
|
|
88
|
-
end
|
|
86
|
+
if @close_deadline && Time.now >= @close_deadline
|
|
87
|
+
@io.close unless @io.closed?
|
|
88
|
+
return false
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
wait_readable = wait_writable = false
|
|
92
|
+
|
|
91
93
|
while true
|
|
92
94
|
read_buff = @io.read_nonblock(4096, exception: false)
|
|
93
95
|
case read_buff
|
|
@@ -100,6 +102,16 @@ class SlackSocketModeBot::SimpleWebSocket
|
|
|
100
102
|
end
|
|
101
103
|
end
|
|
102
104
|
|
|
105
|
+
unless @write_buff.empty?
|
|
106
|
+
len = @io.write_nonblock(@write_buff, exception: false)
|
|
107
|
+
case len
|
|
108
|
+
when :wait_readable then wait_readable = true
|
|
109
|
+
when :wait_writable then wait_writable = true
|
|
110
|
+
else
|
|
111
|
+
@write_buff.clear
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
103
115
|
read_ios << @io if wait_readable
|
|
104
116
|
write_ios << @io if wait_writable
|
|
105
117
|
return true
|
|
@@ -109,6 +121,7 @@ class SlackSocketModeBot::SimpleWebSocket
|
|
|
109
121
|
@fib.raise(EOFError)
|
|
110
122
|
rescue FiberError
|
|
111
123
|
end
|
|
112
|
-
|
|
124
|
+
@io.close unless @io.closed?
|
|
125
|
+
false
|
|
113
126
|
end
|
|
114
127
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SlackSocketModeBot
|
|
4
|
+
# The offline test double exposed as bot.test_mode (bot built with token: "test").
|
|
5
|
+
# It drives the real dispatch without touching the network, and stubs the Web API.
|
|
6
|
+
class TestMode
|
|
7
|
+
def initialize(bot)
|
|
8
|
+
@bot = bot
|
|
9
|
+
@web_api_stubs = {}
|
|
10
|
+
@seq = 0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Forget stubs and dedup state. Returns self.
|
|
14
|
+
def reset
|
|
15
|
+
@web_api_stubs.clear
|
|
16
|
+
@bot.instance_variable_get(:@events).clear
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Stub the Web API response for a method; the block receives the call data.
|
|
21
|
+
def stub_web_api(method, &block)
|
|
22
|
+
@web_api_stubs[method] = block
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Called in place of the real Web API request; returns the stub's value.
|
|
27
|
+
def call(method, data)
|
|
28
|
+
stub = @web_api_stubs[method]
|
|
29
|
+
return stub.call(data) if stub
|
|
30
|
+
warn "unstubbed Web API call: #{ method }" if $VERBOSE
|
|
31
|
+
{ ok: true }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Deliver an envelope as if it arrived over the socket; returns the ACK frames.
|
|
35
|
+
def simulate_envelope(envelope)
|
|
36
|
+
envelope = JSON.parse(JSON.generate(envelope), symbolize_names: true)
|
|
37
|
+
envelope[:envelope_id] ||= "test-envelope-#{ @seq += 1 }"
|
|
38
|
+
produced = []
|
|
39
|
+
@bot.send(
|
|
40
|
+
:handle_message,
|
|
41
|
+
"test",
|
|
42
|
+
JSON.generate(envelope),
|
|
43
|
+
send: ->(text) { produced << JSON.parse(text, symbolize_names: true) },
|
|
44
|
+
close: -> {},
|
|
45
|
+
)
|
|
46
|
+
produced
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -27,6 +27,9 @@ class SlackSocketModeBot
|
|
|
27
27
|
# Max seconds to sleep on a 429 Retry-After; #call may run in the event loop.
|
|
28
28
|
RETRY_AFTER_CAP = 60
|
|
29
29
|
|
|
30
|
+
# #run wakes at least this often so an empty watch set never blocks forever.
|
|
31
|
+
SELECT_TIMEOUT = 30
|
|
32
|
+
|
|
30
33
|
#: (token: String, ?app_token: String, ?num_of_connections: Integer, ?debug: boolean, ?logger: Logger) { (untyped) -> untyped } -> void
|
|
31
34
|
def initialize(token:, app_token: nil, num_of_connections: 4, debug: false, logger: nil, &callback)
|
|
32
35
|
@token = token
|
|
@@ -36,13 +39,32 @@ class SlackSocketModeBot
|
|
|
36
39
|
@logger = logger
|
|
37
40
|
@events = {}
|
|
38
41
|
@callback = callback
|
|
42
|
+
@test_mode = nil
|
|
43
|
+
if token == "test"
|
|
44
|
+
# Offline test double; #call is stubbed and dispatch is driven without connecting.
|
|
45
|
+
require_relative "slack_socket_mode_bot/test_mode"
|
|
46
|
+
@test_mode = TestMode.new(self)
|
|
47
|
+
end
|
|
48
|
+
# app_token and the handler come as a pair, else the bot hangs or crashes on the first event.
|
|
49
|
+
raise ArgumentError, "app_token is required on Socket Mode" if callback && !app_token
|
|
50
|
+
raise ArgumentError, "block is required on Socket Mode" if app_token && !callback
|
|
39
51
|
# No app token: Web API calls only, no Socket Mode connections.
|
|
40
52
|
@num_of_connections = app_token ? num_of_connections : 0
|
|
41
|
-
replenish_connections
|
|
53
|
+
replenish_connections unless test_mode?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The offline test double when built with token: "test", otherwise nil.
|
|
57
|
+
attr_reader :test_mode
|
|
58
|
+
|
|
59
|
+
#: -> bool
|
|
60
|
+
def test_mode?
|
|
61
|
+
!!@test_mode
|
|
42
62
|
end
|
|
43
63
|
|
|
44
64
|
#: (String method, untyped data, ?token: String) -> untyped
|
|
45
65
|
def call(method, data, token: @token)
|
|
66
|
+
return @test_mode.call(method, data) if @test_mode
|
|
67
|
+
|
|
46
68
|
url = URI(API_BASE + method)
|
|
47
69
|
body = JSON.generate(data)
|
|
48
70
|
headers = {
|
|
@@ -82,10 +104,9 @@ class SlackSocketModeBot
|
|
|
82
104
|
end
|
|
83
105
|
|
|
84
106
|
private def replenish_connections
|
|
85
|
-
|
|
86
|
-
while @conns.size < @num_of_connections
|
|
107
|
+
while @conns.count {|ws| !ws.closing? } < @num_of_connections
|
|
87
108
|
begin
|
|
88
|
-
add_connection
|
|
109
|
+
add_connection
|
|
89
110
|
rescue => e
|
|
90
111
|
@logger.warn("[reconnect] failed: #{ e.message }") if @logger
|
|
91
112
|
break
|
|
@@ -95,7 +116,7 @@ class SlackSocketModeBot
|
|
|
95
116
|
raise Error, "all socket connections lost" if @num_of_connections > 0 && @conns.empty?
|
|
96
117
|
end
|
|
97
118
|
|
|
98
|
-
private def add_connection
|
|
119
|
+
private def add_connection
|
|
99
120
|
json = call("apps.connections.open", {}, token: @app_token)
|
|
100
121
|
|
|
101
122
|
url = json[:url]
|
|
@@ -108,64 +129,69 @@ class SlackSocketModeBot
|
|
|
108
129
|
# #step drops the dead connection; #replenish_connections reopens it.
|
|
109
130
|
@logger.info("[ws:#{ ws.object_id }] websocket closed") if @logger
|
|
110
131
|
when :message
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
rescue JSON::ParserError
|
|
114
|
-
# A stray non-JSON frame: skip it, don't open a spurious connection.
|
|
115
|
-
@logger.warn("[ws:#{ ws.object_id }] received a non-JSON message; ignored") if @logger
|
|
116
|
-
next
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
if @logger
|
|
120
|
-
@logger.debug("[ws:#{ ws.object_id }] slack message: #{ JSON.generate(json) }")
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
case json[:type]
|
|
124
|
-
when "hello"
|
|
125
|
-
@logger.info("[ws:#{ ws.object_id }] hello (active connections: #{ @conns.size })") if @logger
|
|
126
|
-
when "disconnect"
|
|
127
|
-
ws.close
|
|
128
|
-
@logger.info("[ws:#{ ws.object_id }] disconnect (active connections: #{ @conns.size })") if @logger
|
|
129
|
-
else
|
|
130
|
-
payload = json[:payload]
|
|
131
|
-
if @logger
|
|
132
|
-
# Log a per-type identifier; event_id/retry only apply to events_api.
|
|
133
|
-
detail =
|
|
134
|
-
case json[:type]
|
|
135
|
-
when "events_api" then [payload.dig(:event, :type), payload[:event_id]].compact.join(" ")
|
|
136
|
-
when "slash_commands" then payload[:command]
|
|
137
|
-
else payload[:type]
|
|
138
|
-
end
|
|
139
|
-
retry_n = json[:retry_attempt].to_i
|
|
140
|
-
line = "[ws:#{ ws.object_id }] #{ json[:type] }"
|
|
141
|
-
line += " #{ detail }" if detail && !detail.empty?
|
|
142
|
-
line += " (retry ##{ retry_n })" if retry_n > 0
|
|
143
|
-
@logger.info(line)
|
|
144
|
-
end
|
|
145
|
-
# Only events_api has an event_id; dedup just those (others have none).
|
|
146
|
-
event_id = payload[:event_id]
|
|
147
|
-
expired = Time.now.to_i - 600
|
|
148
|
-
@events.reject! {|_, timestamp| timestamp < expired }
|
|
149
|
-
|
|
150
|
-
# ACK every message; only skip the handler for a duplicate, else Slack resends.
|
|
151
|
-
duplicate = event_id && @events[event_id]
|
|
152
|
-
@events[event_id] = payload[:event_time] if event_id
|
|
153
|
-
|
|
154
|
-
response = { envelope_id: json[:envelope_id] }
|
|
155
|
-
unless duplicate
|
|
156
|
-
result = callback.call(json)
|
|
157
|
-
response[:payload] = result if json[:accepts_response_payload]
|
|
158
|
-
end
|
|
159
|
-
ws.send(JSON.generate(response))
|
|
160
|
-
end
|
|
132
|
+
# `send` delivers a reply frame; `close` tears the socket down.
|
|
133
|
+
handle_message(ws.object_id, data, send: ->(text) { ws.send(text) }, close: -> { ws.close })
|
|
161
134
|
end
|
|
162
135
|
end
|
|
163
136
|
|
|
164
137
|
@conns << ws
|
|
165
138
|
end
|
|
166
139
|
|
|
140
|
+
# Dispatch one frame (log, dedup, run callback, ACK); send/close are injected so tests can drive it.
|
|
141
|
+
private def handle_message(id, data, send:, close:)
|
|
142
|
+
begin
|
|
143
|
+
json = JSON.parse(data, symbolize_names: true)
|
|
144
|
+
rescue JSON::ParserError
|
|
145
|
+
# A stray non-JSON frame: skip it, don't open a spurious connection.
|
|
146
|
+
@logger.warn("[ws:#{ id }] received a non-JSON message; ignored") if @logger
|
|
147
|
+
return
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
@logger.debug("[ws:#{ id }] slack message: #{ JSON.generate(json) }") if @logger
|
|
151
|
+
|
|
152
|
+
case json[:type]
|
|
153
|
+
when "hello"
|
|
154
|
+
@logger.info("[ws:#{ id }] hello (active connections: #{ @conns.size })") if @logger
|
|
155
|
+
when "disconnect"
|
|
156
|
+
close.call
|
|
157
|
+
@logger.info("[ws:#{ id }] disconnect (active connections: #{ @conns.size })") if @logger
|
|
158
|
+
else
|
|
159
|
+
payload = json[:payload]
|
|
160
|
+
if @logger
|
|
161
|
+
# Log a per-type identifier; event_id/retry only apply to events_api.
|
|
162
|
+
detail =
|
|
163
|
+
case json[:type]
|
|
164
|
+
when "events_api" then [payload.dig(:event, :type), payload[:event_id]].compact.join(" ")
|
|
165
|
+
when "slash_commands" then payload[:command]
|
|
166
|
+
else payload[:type]
|
|
167
|
+
end
|
|
168
|
+
retry_n = json[:retry_attempt].to_i
|
|
169
|
+
line = "[ws:#{ id }] #{ json[:type] }"
|
|
170
|
+
line += " #{ detail }" if detail && !detail.empty?
|
|
171
|
+
line += " (retry ##{ retry_n })" if retry_n > 0
|
|
172
|
+
@logger.info(line)
|
|
173
|
+
end
|
|
174
|
+
# Only events_api has an event_id; dedup just those (others have none).
|
|
175
|
+
event_id = payload[:event_id]
|
|
176
|
+
expired = Time.now.to_i - 600
|
|
177
|
+
@events.reject! {|_, timestamp| timestamp < expired }
|
|
178
|
+
|
|
179
|
+
# ACK every message; only skip the handler for a duplicate, else Slack resends.
|
|
180
|
+
duplicate = event_id && @events[event_id]
|
|
181
|
+
@events[event_id] = payload[:event_time] if event_id
|
|
182
|
+
|
|
183
|
+
response = { envelope_id: json[:envelope_id] }
|
|
184
|
+
unless duplicate
|
|
185
|
+
result = @callback.call(json)
|
|
186
|
+
response[:payload] = result if json[:accepts_response_payload]
|
|
187
|
+
end
|
|
188
|
+
send.call(JSON.generate(response))
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
167
192
|
#: -> [Array[IO], Array[IO]]
|
|
168
193
|
def step
|
|
194
|
+
raise Error, "not supported in test mode" if @test_mode
|
|
169
195
|
read_ios, write_ios = [], []
|
|
170
196
|
@conns.select! {|ws| ws.step(read_ios, write_ios) }
|
|
171
197
|
replenish_connections
|
|
@@ -176,7 +202,7 @@ class SlackSocketModeBot
|
|
|
176
202
|
def run
|
|
177
203
|
while true
|
|
178
204
|
read_ios, write_ios = step
|
|
179
|
-
IO.select(read_ios, write_ios)
|
|
205
|
+
IO.select(read_ios, write_ios, nil, SELECT_TIMEOUT)
|
|
180
206
|
end
|
|
181
207
|
end
|
|
182
208
|
end
|
|
@@ -11,4 +11,16 @@ class SlackSocketModeBot
|
|
|
11
11
|
def step: () -> [ Array[IO], Array[IO] ]
|
|
12
12
|
|
|
13
13
|
def run: () -> void
|
|
14
|
+
|
|
15
|
+
def test_mode?: () -> bool
|
|
16
|
+
|
|
17
|
+
def test_mode: () -> TestMode?
|
|
18
|
+
|
|
19
|
+
class TestMode
|
|
20
|
+
def stub_web_api: (String method) { (untyped) -> untyped } -> self
|
|
21
|
+
|
|
22
|
+
def simulate_envelope: (untyped envelope) -> Array[untyped]
|
|
23
|
+
|
|
24
|
+
def reset: () -> self
|
|
25
|
+
end
|
|
14
26
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: slack_socket_mode_bot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yusuke Endoh
|
|
@@ -36,8 +36,9 @@ files:
|
|
|
36
36
|
- Rakefile
|
|
37
37
|
- lib/slack_socket_mode_bot.rb
|
|
38
38
|
- lib/slack_socket_mode_bot/simple_web_socket.rb
|
|
39
|
+
- lib/slack_socket_mode_bot/test_mode.rb
|
|
39
40
|
- lib/slack_socket_mode_bot/version.rb
|
|
40
|
-
- sig/
|
|
41
|
+
- sig/slack_socket_mode_bot.rbs
|
|
41
42
|
homepage: "https://github.com/mame/slack_socket_mode_bot"
|
|
42
43
|
licenses:
|
|
43
44
|
- MIT
|