slack_socket_mode_bot 0.9.0 → 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/LICENSE +21 -0
- data/README.md +16 -12
- data/Rakefile +9 -1
- data/lib/slack_socket_mode_bot/version.rb +1 -1
- data/lib/slack_socket_mode_bot.rb +100 -26
- data/sig/slack_socket_mode.rbs +12 -11
- metadata +42 -36
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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Yusuke Endoh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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
|
|
|
@@ -94,6 +90,9 @@ Connects to Slack with Socket Mode.
|
|
|
94
90
|
* `logger`: A Logger instance (optional)
|
|
95
91
|
* block: Handles events received from Slack
|
|
96
92
|
|
|
93
|
+
Note: The block must return as soon as possible. Otherwise, the Slack server will re-send the event.
|
|
94
|
+
If you want to do a time-consuming process, it is recommended that you do it in a sub thread.
|
|
95
|
+
|
|
97
96
|
### `SlackSocketModeBot#call(method, data, token:)`
|
|
98
97
|
|
|
99
98
|
Calls Slack's [Web API](https://api.slack.com/methods), such as [chat.postMessage](https://api.slack.com/methods/chat.postMessage).
|
|
@@ -107,6 +106,11 @@ This method returns the response as a JSON data.
|
|
|
107
106
|
|
|
108
107
|
Starts the main loop of communication with Slack. This method does not return.
|
|
109
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
|
+
|
|
110
114
|
### `SlackSocketModeBot#step`
|
|
111
115
|
|
|
112
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
|
|
@@ -19,30 +34,67 @@ class SlackSocketModeBot
|
|
|
19
34
|
@conns = []
|
|
20
35
|
@debug = debug
|
|
21
36
|
@logger = logger
|
|
22
|
-
|
|
37
|
+
@events = {}
|
|
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
|
|
23
42
|
end
|
|
24
43
|
|
|
25
44
|
#: (String method, untyped data, ?token: String) -> untyped
|
|
26
45
|
def call(method, data, token: @token)
|
|
27
|
-
|
|
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
|
|
28
54
|
begin
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
39
73
|
sleep 1
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
43
81
|
end
|
|
44
82
|
end
|
|
45
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
|
|
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?
|
|
96
|
+
end
|
|
97
|
+
|
|
46
98
|
private def add_connection(callback)
|
|
47
99
|
json = call("apps.connections.open", {}, token: @app_token)
|
|
48
100
|
|
|
@@ -53,35 +105,56 @@ class SlackSocketModeBot
|
|
|
53
105
|
when :open
|
|
54
106
|
@logger.info("[ws:#{ ws.object_id }] websocket open") if @logger
|
|
55
107
|
when :close
|
|
108
|
+
# #step drops the dead connection; #replenish_connections reopens it.
|
|
56
109
|
@logger.info("[ws:#{ ws.object_id }] websocket closed") if @logger
|
|
57
|
-
add_connection(callback)
|
|
58
110
|
when :message
|
|
59
111
|
begin
|
|
60
112
|
json = JSON.parse(data, symbolize_names: true)
|
|
61
113
|
rescue JSON::ParserError
|
|
62
|
-
|
|
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
|
|
63
116
|
next
|
|
64
117
|
end
|
|
65
118
|
|
|
66
119
|
if @logger
|
|
67
120
|
@logger.debug("[ws:#{ ws.object_id }] slack message: #{ JSON.generate(json) }")
|
|
68
|
-
msg = "slack #{ json[:type] }"
|
|
69
|
-
payload_type = json.dig(:payload, :type)
|
|
70
|
-
msg += " (#{ payload_type })" if payload_type
|
|
71
|
-
@logger.info("[ws:#{ ws.object_id }] " + msg)
|
|
72
121
|
end
|
|
73
122
|
|
|
74
123
|
case json[:type]
|
|
75
124
|
when "hello"
|
|
76
|
-
@logger.info("[ws:#{ ws.object_id }] active
|
|
125
|
+
@logger.info("[ws:#{ ws.object_id }] hello (active connections: #{ @conns.size })") if @logger
|
|
77
126
|
when "disconnect"
|
|
78
127
|
ws.close
|
|
128
|
+
@logger.info("[ws:#{ ws.object_id }] disconnect (active connections: #{ @conns.size })") if @logger
|
|
79
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
|
+
|
|
80
154
|
response = { envelope_id: json[:envelope_id] }
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
callback.call(json)
|
|
155
|
+
unless duplicate
|
|
156
|
+
result = callback.call(json)
|
|
157
|
+
response[:payload] = result if json[:accepts_response_payload]
|
|
85
158
|
end
|
|
86
159
|
ws.send(JSON.generate(response))
|
|
87
160
|
end
|
|
@@ -95,6 +168,7 @@ class SlackSocketModeBot
|
|
|
95
168
|
def step
|
|
96
169
|
read_ios, write_ios = [], []
|
|
97
170
|
@conns.select! {|ws| ws.step(read_ios, write_ios) }
|
|
171
|
+
replenish_connections
|
|
98
172
|
return read_ios, write_ios
|
|
99
173
|
end
|
|
100
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,60 +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
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
- lib/slack_socket_mode_bot
|
|
36
|
-
- lib/slack_socket_mode_bot/
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
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"
|
|
42
|
+
licenses:
|
|
43
|
+
- MIT
|
|
40
44
|
metadata:
|
|
41
|
-
homepage_uri: https://github.com/mame/slack_socket_mode_bot
|
|
42
|
-
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"
|
|
43
47
|
rdoc_options: []
|
|
44
48
|
require_paths:
|
|
45
|
-
- lib
|
|
49
|
+
- lib
|
|
46
50
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
47
51
|
requirements:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
-
|
|
53
|
+
- ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: 3.3.0
|
|
51
56
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
57
|
requirements:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
-
|
|
59
|
+
- ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: "0"
|
|
56
62
|
requirements: []
|
|
57
|
-
rubygems_version:
|
|
63
|
+
rubygems_version: 4.1.0.dev
|
|
58
64
|
specification_version: 4
|
|
59
65
|
summary: A simple wrapper library for Slack's Socket Mode API
|
|
60
66
|
test_files: []
|