slack_socket_mode_bot 0.9.1 → 0.9.2
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 +13 -12
- data/Rakefile +9 -1
- data/lib/slack_socket_mode_bot/version.rb +1 -1
- data/lib/slack_socket_mode_bot.rb +93 -37
- data/sig/slack_socket_mode.rbs +12 -11
- metadata +41 -37
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ecdae100a7f27d51751fe1fd77c772ca8a88cfab323266c874b996ffaed909c8
|
|
4
|
+
data.tar.gz: 92b4827386e87485f3adf26e93de16e910a2c4f9d320ba4cc683768d067b130f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 497006f3d3854a431e29f12f6463c57ed35290e5d722de7fe7bab24a886f667483821553da14ecc904d64b6de45c3d48ae446f90ba4424c3fbcbf7c59362ec38
|
|
7
|
+
data.tar.gz: a0e94a4812aaf022a7e62e02f0a56718ecb38cfd49002fc0dbdc7e9833fcb6520dfe799ff3d2279d9239520a615215240337261a4116c4c4ab1b7496d4239af5
|
data/README.md
CHANGED
|
@@ -64,22 +64,18 @@ bot.run
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
```
|
|
67
|
-
$ ruby
|
|
67
|
+
$ ruby examples/echo_bot.rb
|
|
68
68
|
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2560] websocket open
|
|
69
69
|
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2600] websocket open
|
|
70
|
-
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2560] slack hello
|
|
71
|
-
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2560] active connection count: 4
|
|
72
|
-
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2600] slack hello
|
|
73
|
-
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2600] active connection count: 4
|
|
74
70
|
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2640] websocket open
|
|
75
|
-
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2640] slack hello
|
|
76
|
-
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2640] active connection count: 4
|
|
77
71
|
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] websocket open
|
|
78
|
-
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:
|
|
79
|
-
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:
|
|
80
|
-
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:
|
|
81
|
-
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680]
|
|
82
|
-
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680]
|
|
72
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2560] hello (active connections: 4)
|
|
73
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2600] hello (active connections: 4)
|
|
74
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2640] hello (active connections: 4)
|
|
75
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] hello (active connections: 4)
|
|
76
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] events_api app_mention Ev08H3RABCDE
|
|
77
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] events_api app_mention Ev08H3RFGHIJ
|
|
78
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] events_api app_mention Ev08H3RKLMNO
|
|
83
79
|
...
|
|
84
80
|
```
|
|
85
81
|
|
|
@@ -110,6 +106,11 @@ This method returns the response as a JSON data.
|
|
|
110
106
|
|
|
111
107
|
Starts the main loop of communication with Slack. This method does not return.
|
|
112
108
|
|
|
109
|
+
On an unrecoverable error (for example, losing every connection and being unable
|
|
110
|
+
to reopen any), this method raises an exception instead of silently continuing in
|
|
111
|
+
a degraded state. Running your bot under a process supervisor such as systemd is
|
|
112
|
+
recommended so that it is restarted when this happens.
|
|
113
|
+
|
|
113
114
|
### `SlackSocketModeBot#step`
|
|
114
115
|
|
|
115
116
|
Proceeds with the communication one step.
|
data/Rakefile
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "bundler/gem_tasks"
|
|
4
|
-
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << "lib" << "test"
|
|
8
|
+
t.test_files = FileList["test/test_*.rb"]
|
|
9
|
+
t.warning = false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
task default: %i[test]
|
|
@@ -12,6 +12,21 @@ require_relative "slack_socket_mode_bot/simple_web_socket"
|
|
|
12
12
|
class SlackSocketModeBot
|
|
13
13
|
class Error < StandardError; end
|
|
14
14
|
|
|
15
|
+
API_BASE = "https://slack.com/api/"
|
|
16
|
+
|
|
17
|
+
# Internal signal: retry after `wait` seconds. Always caught in #call.
|
|
18
|
+
class Retry < StandardError
|
|
19
|
+
attr_reader :wait
|
|
20
|
+
def initialize(wait)
|
|
21
|
+
@wait = wait
|
|
22
|
+
super("retry after #{ wait }s")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
private_constant :Retry
|
|
26
|
+
|
|
27
|
+
# Max seconds to sleep on a 429 Retry-After; #call may run in the event loop.
|
|
28
|
+
RETRY_AFTER_CAP = 60
|
|
29
|
+
|
|
15
30
|
#: (token: String, ?app_token: String, ?num_of_connections: Integer, ?debug: boolean, ?logger: Logger) { (untyped) -> untyped } -> void
|
|
16
31
|
def initialize(token:, app_token: nil, num_of_connections: 4, debug: false, logger: nil, &callback)
|
|
17
32
|
@token = token
|
|
@@ -20,28 +35,64 @@ class SlackSocketModeBot
|
|
|
20
35
|
@debug = debug
|
|
21
36
|
@logger = logger
|
|
22
37
|
@events = {}
|
|
23
|
-
|
|
38
|
+
@callback = callback
|
|
39
|
+
# No app token: Web API calls only, no Socket Mode connections.
|
|
40
|
+
@num_of_connections = app_token ? num_of_connections : 0
|
|
41
|
+
replenish_connections
|
|
24
42
|
end
|
|
25
43
|
|
|
26
44
|
#: (String method, untyped data, ?token: String) -> untyped
|
|
27
45
|
def call(method, data, token: @token)
|
|
28
|
-
|
|
46
|
+
url = URI(API_BASE + method)
|
|
47
|
+
body = JSON.generate(data)
|
|
48
|
+
headers = {
|
|
49
|
+
"Content-type" => "application/json; charset=utf-8",
|
|
50
|
+
"Authorization" => "Bearer " + token,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
retries = 0
|
|
29
54
|
begin
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
res = Net::HTTP.post(url, body, headers)
|
|
56
|
+
|
|
57
|
+
case res
|
|
58
|
+
when Net::HTTPSuccess
|
|
59
|
+
json = JSON.parse(res.body, symbolize_names: true)
|
|
60
|
+
raise Error, json[:error] unless json[:ok]
|
|
61
|
+
json
|
|
62
|
+
when Net::HTTPTooManyRequests
|
|
63
|
+
# Rejected, not processed: safe to retry after Retry-After seconds.
|
|
64
|
+
raise Retry, Integer(res["retry-after"] || retries + 1)
|
|
65
|
+
else
|
|
66
|
+
# 5xx etc.: may already be processed, so don't retry; just don't crash.
|
|
67
|
+
raise Error, "HTTP #{ res.code } #{ res.message }"
|
|
68
|
+
end
|
|
69
|
+
rescue Socket::ResolutionError, Net::OpenTimeout
|
|
70
|
+
# Never sent (DNS / connect failure): safe to retry.
|
|
71
|
+
retries += 1
|
|
72
|
+
raise if retries >= 3
|
|
40
73
|
sleep 1
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
74
|
+
retry
|
|
75
|
+
rescue Retry => e
|
|
76
|
+
# Don't block the event loop on an absurdly long wait.
|
|
77
|
+
retries += 1
|
|
78
|
+
raise Error, "rate limited (retry-after: #{ e.wait }s)" if retries >= 3 || e.wait > RETRY_AFTER_CAP
|
|
79
|
+
sleep e.wait
|
|
80
|
+
retry
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private def replenish_connections
|
|
85
|
+
# Reopen from the main loop, tolerating a single failure; #step retries.
|
|
86
|
+
while @conns.size < @num_of_connections
|
|
87
|
+
begin
|
|
88
|
+
add_connection(@callback)
|
|
89
|
+
rescue => e
|
|
90
|
+
@logger.warn("[reconnect] failed: #{ e.message }") if @logger
|
|
91
|
+
break
|
|
92
|
+
end
|
|
44
93
|
end
|
|
94
|
+
# Fail loud rather than degrade silently once every connection is gone.
|
|
95
|
+
raise Error, "all socket connections lost" if @num_of_connections > 0 && @conns.empty?
|
|
45
96
|
end
|
|
46
97
|
|
|
47
98
|
private def add_connection(callback)
|
|
@@ -54,13 +105,14 @@ class SlackSocketModeBot
|
|
|
54
105
|
when :open
|
|
55
106
|
@logger.info("[ws:#{ ws.object_id }] websocket open") if @logger
|
|
56
107
|
when :close
|
|
108
|
+
# #step drops the dead connection; #replenish_connections reopens it.
|
|
57
109
|
@logger.info("[ws:#{ ws.object_id }] websocket closed") if @logger
|
|
58
|
-
add_connection(callback)
|
|
59
110
|
when :message
|
|
60
111
|
begin
|
|
61
112
|
json = JSON.parse(data, symbolize_names: true)
|
|
62
113
|
rescue JSON::ParserError
|
|
63
|
-
|
|
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
|
|
64
116
|
next
|
|
65
117
|
end
|
|
66
118
|
|
|
@@ -77,31 +129,34 @@ class SlackSocketModeBot
|
|
|
77
129
|
else
|
|
78
130
|
payload = json[:payload]
|
|
79
131
|
if @logger
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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)
|
|
88
144
|
end
|
|
145
|
+
# Only events_api has an event_id; dedup just those (others have none).
|
|
146
|
+
event_id = payload[:event_id]
|
|
89
147
|
expired = Time.now.to_i - 600
|
|
90
148
|
@events.reject! {|_, timestamp| timestamp < expired }
|
|
91
149
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
else
|
|
101
|
-
callback.call(json)
|
|
102
|
-
end
|
|
103
|
-
ws.send(JSON.generate(response))
|
|
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]
|
|
104
158
|
end
|
|
159
|
+
ws.send(JSON.generate(response))
|
|
105
160
|
end
|
|
106
161
|
end
|
|
107
162
|
end
|
|
@@ -113,6 +168,7 @@ class SlackSocketModeBot
|
|
|
113
168
|
def step
|
|
114
169
|
read_ios, write_ios = [], []
|
|
115
170
|
@conns.select! {|ws| ws.step(read_ios, write_ios) }
|
|
171
|
+
replenish_connections
|
|
116
172
|
return read_ios, write_ios
|
|
117
173
|
end
|
|
118
174
|
|
data/sig/slack_socket_mode.rbs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def initialize: (token: String, ?app_token: String, ?num_of_connections: Integer, ?debug: boolean, ?logger: Logger) { (untyped) -> untyped } -> void
|
|
6
|
-
|
|
7
|
-
def call: (String method, untyped data, ?token: String) -> untyped
|
|
8
|
-
|
|
9
|
-
def step: -> [Array[IO], Array[IO]]
|
|
10
|
-
|
|
11
|
-
def run: -> bot
|
|
1
|
+
class SlackSocketModeBot
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
12
5
|
end
|
|
6
|
+
|
|
7
|
+
def initialize: (token: String, ?app_token: String?, ?num_of_connections: Integer, ?debug: bool, ?logger: Logger?) { (untyped) -> untyped } -> void
|
|
8
|
+
|
|
9
|
+
def call: (String method, untyped data, ?token: String) -> untyped
|
|
10
|
+
|
|
11
|
+
def step: () -> [ Array[IO], Array[IO] ]
|
|
12
|
+
|
|
13
|
+
def run: () -> void
|
|
13
14
|
end
|
metadata
CHANGED
|
@@ -1,62 +1,66 @@
|
|
|
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
- Yusuke Endoh
|
|
7
|
+
- Yusuke Endoh
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: websocket
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
-
|
|
17
|
+
- ~>
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: "1.2"
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
-
|
|
25
|
+
- ~>
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: "1.2"
|
|
26
28
|
email:
|
|
27
|
-
- mame@ruby-lang.org
|
|
29
|
+
- mame@ruby-lang.org
|
|
28
30
|
executables: []
|
|
29
31
|
extensions: []
|
|
30
32
|
extra_rdoc_files: []
|
|
31
33
|
files:
|
|
32
|
-
- LICENSE
|
|
33
|
-
- README.md
|
|
34
|
-
- Rakefile
|
|
35
|
-
- lib/slack_socket_mode_bot.rb
|
|
36
|
-
- lib/slack_socket_mode_bot/simple_web_socket.rb
|
|
37
|
-
- lib/slack_socket_mode_bot/version.rb
|
|
38
|
-
- sig/slack_socket_mode.rbs
|
|
39
|
-
homepage: https://github.com/mame/slack_socket_mode_bot
|
|
34
|
+
- LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- Rakefile
|
|
37
|
+
- lib/slack_socket_mode_bot.rb
|
|
38
|
+
- lib/slack_socket_mode_bot/simple_web_socket.rb
|
|
39
|
+
- lib/slack_socket_mode_bot/version.rb
|
|
40
|
+
- sig/slack_socket_mode.rbs
|
|
41
|
+
homepage: "https://github.com/mame/slack_socket_mode_bot"
|
|
40
42
|
licenses:
|
|
41
|
-
- MIT
|
|
43
|
+
- MIT
|
|
42
44
|
metadata:
|
|
43
|
-
homepage_uri: https://github.com/mame/slack_socket_mode_bot
|
|
44
|
-
source_code_uri: https://github.com/mame/slack_socket_mode_bot
|
|
45
|
+
homepage_uri: "https://github.com/mame/slack_socket_mode_bot"
|
|
46
|
+
source_code_uri: "https://github.com/mame/slack_socket_mode_bot"
|
|
45
47
|
rdoc_options: []
|
|
46
48
|
require_paths:
|
|
47
|
-
- lib
|
|
49
|
+
- lib
|
|
48
50
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
51
|
requirements:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
-
|
|
53
|
+
- ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 3.3.0
|
|
53
56
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
57
|
requirements:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
-
|
|
59
|
+
- ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: "0"
|
|
58
62
|
requirements: []
|
|
59
|
-
rubygems_version:
|
|
63
|
+
rubygems_version: 4.1.0.dev
|
|
60
64
|
specification_version: 4
|
|
61
65
|
summary: A simple wrapper library for Slack's Socket Mode API
|
|
62
66
|
test_files: []
|