slack_socket_mode_bot 0.9.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.
- checksums.yaml +7 -0
- data/README.md +127 -0
- data/Rakefile +4 -0
- data/lib/slack_socket_mode_bot/simple_web_socket.rb +114 -0
- data/lib/slack_socket_mode_bot/version.rb +5 -0
- data/lib/slack_socket_mode_bot.rb +108 -0
- data/sig/slack_socket_mode.rbs +13 -0
- metadata +60 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ea84a2e36d8b349ef867322ad4e932703dc5fa7eea1397f693268977aecfb001
|
|
4
|
+
data.tar.gz: 1802e47527f56357fa5a3d564f61f91a6a3f3741bb107227dad18be8beedb08f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4b5cbb57859b8f34a2d45c8869d2f197ef84f24d7554c12027e331a783edc510357dd6d7cd1b199db98870b17abb2a27e2ca1f70fc4fce0151fa82392db4456a
|
|
7
|
+
data.tar.gz: e84f97c7a0dbe89e249315f5142b7c275de3b7cfb4f90127727d7d9e943f14c1eb493994a56554d8af22c7a2248e9e5a9c873a2a08e3f21bd7614265d5fa383b
|
data/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# SlackSocketModeBot
|
|
2
|
+
|
|
3
|
+
This is a simple Ruby wrapper for the [Slack's Socket Mode API](https://api.slack.com/apis/socket-mode).
|
|
4
|
+
It allows you to write a Slack bot without exposing a public HTTP endpoint.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
First, set up your Slack app for Socket Mode by reading [the official document](https://api.slack.com/apis/socket-mode).
|
|
9
|
+
|
|
10
|
+
Then, run the following script.
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
# simple echo bot
|
|
14
|
+
|
|
15
|
+
require "slack_socket_mode_bot"
|
|
16
|
+
require "logger"
|
|
17
|
+
|
|
18
|
+
# Slack's Bot User OAuth Token
|
|
19
|
+
# You can create this token with: https://api.slack.com/apps/ - "OAuth & Permissions" - "OAuth Tokens for Your Workspace"
|
|
20
|
+
SLACK_BOT_TOKEN = "xoxb-..."
|
|
21
|
+
|
|
22
|
+
# Slack's App-Level Token
|
|
23
|
+
# You can create this token with: https://api.slack.com/apps/ - "Basic Information" - "App-Level Tokens"
|
|
24
|
+
SLACK_APP_TOKEN = "xapp-..."
|
|
25
|
+
|
|
26
|
+
logger = Logger.new(STDOUT, level: Logger::Severity::INFO)
|
|
27
|
+
|
|
28
|
+
bot = SlackSocketModeBot.new(token: SLACK_BOT_TOKEN, app_token: SLACK_APP_TOKEN, logger: logger) do |data|
|
|
29
|
+
# Event handler. A sample data is
|
|
30
|
+
#
|
|
31
|
+
# {
|
|
32
|
+
# "type": "events_api",
|
|
33
|
+
# "envelope_id": "...",
|
|
34
|
+
# "accepts_response_payload": false,
|
|
35
|
+
# "payload": {
|
|
36
|
+
# "type": "event_callback",
|
|
37
|
+
# "event": {
|
|
38
|
+
# "type": "app_mention",
|
|
39
|
+
# "text": "hello",
|
|
40
|
+
# ...
|
|
41
|
+
# },
|
|
42
|
+
# ...
|
|
43
|
+
# }
|
|
44
|
+
# }
|
|
45
|
+
#
|
|
46
|
+
# See https://api.slack.com/apis/socket-mode#events in detail
|
|
47
|
+
|
|
48
|
+
if data[:type] == "events_api" && data[:payload][:event][:type] == "app_mention"
|
|
49
|
+
event = data[:payload][:event]
|
|
50
|
+
|
|
51
|
+
text = event[:text]
|
|
52
|
+
|
|
53
|
+
echo_text = "echo:" + text
|
|
54
|
+
|
|
55
|
+
bot.call("chat.postMessage", { channel: event[:channel], text: echo_text })
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
rescue Exception
|
|
59
|
+
puts $!.full_message
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Start the communication
|
|
63
|
+
bot.run
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
$ ruby example/echo_bot.rb
|
|
68
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2560] websocket open
|
|
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
|
+
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
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] websocket open
|
|
78
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] slack hello
|
|
79
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] active connection count: 4
|
|
80
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] slack events_api (event_callback)
|
|
81
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] slack events_api (event_callback)
|
|
82
|
+
I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] slack events_api (event_callback)
|
|
83
|
+
...
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
### `SlackSocketModeBot.new(token:, app_token:, logger:)`
|
|
89
|
+
|
|
90
|
+
Connects to Slack with Socket Mode.
|
|
91
|
+
|
|
92
|
+
* `token`: Slack's Bot User OAuth token (starting with `xoxb-`)
|
|
93
|
+
* `app_token`: Slack's App-Level token (starting with `xapp-`)
|
|
94
|
+
* `logger`: A Logger instance (optional)
|
|
95
|
+
* block: Handles events received from Slack
|
|
96
|
+
|
|
97
|
+
### `SlackSocketModeBot#call(method, data, token:)`
|
|
98
|
+
|
|
99
|
+
Calls Slack's [Web API](https://api.slack.com/methods), such as [chat.postMessage](https://api.slack.com/methods/chat.postMessage).
|
|
100
|
+
|
|
101
|
+
* `method`: API name (such as `"chat.postMessage"`)
|
|
102
|
+
* `data`: Arguments
|
|
103
|
+
|
|
104
|
+
This method returns the response as a JSON data.
|
|
105
|
+
|
|
106
|
+
### `SlackSocketModeBot#run`
|
|
107
|
+
|
|
108
|
+
Starts the main loop of communication with Slack. This method does not return.
|
|
109
|
+
|
|
110
|
+
### `SlackSocketModeBot#step`
|
|
111
|
+
|
|
112
|
+
Proceeds with the communication one step.
|
|
113
|
+
|
|
114
|
+
This method returns an array of IO waiting to be readable and an array of IO waiting to be writable.
|
|
115
|
+
They are supposed to be passed to `IO.select`.
|
|
116
|
+
|
|
117
|
+
Typically, this method should be used as follows.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
while true
|
|
121
|
+
read_ios, write_ios = app.step
|
|
122
|
+
IO.select(read_ios, write_ios)
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This method allows you to manage the main loop yourself.
|
|
127
|
+
If you don't need it, you can just use `SlackSocketModeBot#run`.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
class SlackSocketModeBot::SimpleWebSocket
|
|
2
|
+
def initialize(url)
|
|
3
|
+
uri = URI.parse(url)
|
|
4
|
+
|
|
5
|
+
unless uri.scheme == "https" || uri.scheme == "wss"
|
|
6
|
+
raise "unexpected scheme (not secure?): #{ uri.scheme }"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
10
|
+
ctx.ssl_version = "SSLv23"
|
|
11
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
12
|
+
ctx.cert_store = OpenSSL::X509::Store.new
|
|
13
|
+
ctx.cert_store.set_default_paths
|
|
14
|
+
|
|
15
|
+
count = 0
|
|
16
|
+
begin
|
|
17
|
+
io = TCPSocket.new(uri.host, uri.port || 443)
|
|
18
|
+
@io = OpenSSL::SSL::SSLSocket.new(io, ctx)
|
|
19
|
+
@io.connect
|
|
20
|
+
rescue Socket::ResolutionError
|
|
21
|
+
sleep 1
|
|
22
|
+
count += 1
|
|
23
|
+
retry if count < 3
|
|
24
|
+
raise
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@version = nil
|
|
28
|
+
|
|
29
|
+
@fib = Fiber.new do
|
|
30
|
+
closed = false
|
|
31
|
+
begin
|
|
32
|
+
handshake = WebSocket::Handshake::Client.new(url: url)
|
|
33
|
+
@write_buff = handshake.to_s.dup
|
|
34
|
+
handshake << Fiber.yield until handshake.finished?
|
|
35
|
+
|
|
36
|
+
@version = handshake.version
|
|
37
|
+
yield :open
|
|
38
|
+
|
|
39
|
+
frame = WebSocket::Frame::Incoming::Client.new
|
|
40
|
+
frame << handshake.leftovers
|
|
41
|
+
while true
|
|
42
|
+
while msg = frame.next
|
|
43
|
+
case msg.type
|
|
44
|
+
when :close
|
|
45
|
+
yield :close unless closed
|
|
46
|
+
closed = true
|
|
47
|
+
when :ping
|
|
48
|
+
send(msg.data, type: :pong)
|
|
49
|
+
when :pong
|
|
50
|
+
when :text
|
|
51
|
+
yield :message, msg.data, :text
|
|
52
|
+
when :binary
|
|
53
|
+
yield :message, msg.data, :binary
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
frame << Fiber.yield
|
|
57
|
+
end
|
|
58
|
+
rescue EOFError
|
|
59
|
+
ensure
|
|
60
|
+
yield :close unless closed
|
|
61
|
+
@io.close
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@fib.resume
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def send(data, type: :text, code: nil)
|
|
69
|
+
raise "not opened yet" unless @version
|
|
70
|
+
frame = WebSocket::Frame::Outgoing::Client.new(version: @version, data: data, type: type, code: code)
|
|
71
|
+
@write_buff << frame.to_s
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def close(code: 1000, reason: "")
|
|
75
|
+
send(reason, type: :close, code: code) unless @io.closed?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def step(read_ios, write_ios)
|
|
79
|
+
wait_readable = wait_writable = false
|
|
80
|
+
|
|
81
|
+
unless @write_buff.empty?
|
|
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
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
while true
|
|
92
|
+
read_buff = @io.read_nonblock(4096, exception: false)
|
|
93
|
+
case read_buff
|
|
94
|
+
when :wait_readable then wait_readable = true; break
|
|
95
|
+
when :wait_writable then wait_writable = true; break
|
|
96
|
+
when nil
|
|
97
|
+
raise Errno::EPIPE
|
|
98
|
+
else
|
|
99
|
+
@fib.resume(read_buff)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
read_ios << @io if wait_readable
|
|
104
|
+
write_ios << @io if wait_writable
|
|
105
|
+
return true
|
|
106
|
+
|
|
107
|
+
rescue Errno::EPIPE, Errno::ECONNRESET
|
|
108
|
+
begin
|
|
109
|
+
@fib.raise(EOFError)
|
|
110
|
+
rescue FiberError
|
|
111
|
+
end
|
|
112
|
+
return false
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "websocket"
|
|
7
|
+
require "json"
|
|
8
|
+
|
|
9
|
+
require_relative "slack_socket_mode_bot/version"
|
|
10
|
+
require_relative "slack_socket_mode_bot/simple_web_socket"
|
|
11
|
+
|
|
12
|
+
class SlackSocketModeBot
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
|
|
15
|
+
#: (token: String, ?app_token: String, ?num_of_connections: Integer, ?debug: boolean, ?logger: Logger) { (untyped) -> untyped } -> void
|
|
16
|
+
def initialize(token:, app_token: nil, num_of_connections: 4, debug: false, logger: nil, &callback)
|
|
17
|
+
@token = token
|
|
18
|
+
@app_token = app_token
|
|
19
|
+
@conns = []
|
|
20
|
+
@debug = debug
|
|
21
|
+
@logger = logger
|
|
22
|
+
num_of_connections.times { add_connection(callback) } if app_token
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#: (String method, untyped data, ?token: String) -> untyped
|
|
26
|
+
def call(method, data, token: @token)
|
|
27
|
+
count = 0
|
|
28
|
+
begin
|
|
29
|
+
url = URI("https://slack.com/api/" + method)
|
|
30
|
+
res = Net::HTTP.post(
|
|
31
|
+
url, JSON.generate(data),
|
|
32
|
+
"Content-type" => "application/json; charset=utf-8",
|
|
33
|
+
"Authorization" => "Bearer " + token,
|
|
34
|
+
)
|
|
35
|
+
json = JSON.parse(res.body, symbolize_names: true)
|
|
36
|
+
raise Error, json[:error] unless json[:ok]
|
|
37
|
+
json
|
|
38
|
+
rescue Socket::ResolutionError
|
|
39
|
+
sleep 1
|
|
40
|
+
count += 1
|
|
41
|
+
retry if count < 3
|
|
42
|
+
raise
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private def add_connection(callback)
|
|
47
|
+
json = call("apps.connections.open", {}, token: @app_token)
|
|
48
|
+
|
|
49
|
+
url = json[:url]
|
|
50
|
+
url += "&debug_reconnects=true" if @debug
|
|
51
|
+
ws = SimpleWebSocket.new(url) do |type, data|
|
|
52
|
+
case type
|
|
53
|
+
when :open
|
|
54
|
+
@logger.info("[ws:#{ ws.object_id }] websocket open") if @logger
|
|
55
|
+
when :close
|
|
56
|
+
@logger.info("[ws:#{ ws.object_id }] websocket closed") if @logger
|
|
57
|
+
add_connection(callback)
|
|
58
|
+
when :message
|
|
59
|
+
begin
|
|
60
|
+
json = JSON.parse(data, symbolize_names: true)
|
|
61
|
+
rescue JSON::ParserError
|
|
62
|
+
add_connection(callback)
|
|
63
|
+
next
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if @logger
|
|
67
|
+
@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
|
+
end
|
|
73
|
+
|
|
74
|
+
case json[:type]
|
|
75
|
+
when "hello"
|
|
76
|
+
@logger.info("[ws:#{ ws.object_id }] active connection count: #{ @conns.size }") if @logger
|
|
77
|
+
when "disconnect"
|
|
78
|
+
ws.close
|
|
79
|
+
else
|
|
80
|
+
response = { envelope_id: json[:envelope_id] }
|
|
81
|
+
if json[:accepts_response_payload]
|
|
82
|
+
response[:payload] = callback.call(json)
|
|
83
|
+
else
|
|
84
|
+
callback.call(json)
|
|
85
|
+
end
|
|
86
|
+
ws.send(JSON.generate(response))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@conns << ws
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
#: -> [Array[IO], Array[IO]]
|
|
95
|
+
def step
|
|
96
|
+
read_ios, write_ios = [], []
|
|
97
|
+
@conns.select! {|ws| ws.step(read_ios, write_ios) }
|
|
98
|
+
return read_ios, write_ios
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
#: -> bot
|
|
102
|
+
def run
|
|
103
|
+
while true
|
|
104
|
+
read_ios, write_ios = step
|
|
105
|
+
IO.select(read_ios, write_ios)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Slack
|
|
2
|
+
class SocketMode
|
|
3
|
+
VERSION: String
|
|
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
|
|
12
|
+
end
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: slack_socket_mode_bot
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.9.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yusuke Endoh
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2024-06-21 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: websocket
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.2'
|
|
26
|
+
email:
|
|
27
|
+
- mame@ruby-lang.org
|
|
28
|
+
executables: []
|
|
29
|
+
extensions: []
|
|
30
|
+
extra_rdoc_files: []
|
|
31
|
+
files:
|
|
32
|
+
- README.md
|
|
33
|
+
- Rakefile
|
|
34
|
+
- lib/slack_socket_mode_bot.rb
|
|
35
|
+
- lib/slack_socket_mode_bot/simple_web_socket.rb
|
|
36
|
+
- lib/slack_socket_mode_bot/version.rb
|
|
37
|
+
- sig/slack_socket_mode.rbs
|
|
38
|
+
homepage: https://github.com/mame/slack_socket_mode_bot
|
|
39
|
+
licenses: []
|
|
40
|
+
metadata:
|
|
41
|
+
homepage_uri: https://github.com/mame/slack_socket_mode_bot
|
|
42
|
+
source_code_uri: https://github.com/mame/slack_socket_mode_bot
|
|
43
|
+
rdoc_options: []
|
|
44
|
+
require_paths:
|
|
45
|
+
- lib
|
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: 3.0.0
|
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - ">="
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0'
|
|
56
|
+
requirements: []
|
|
57
|
+
rubygems_version: 3.6.0.dev
|
|
58
|
+
specification_version: 4
|
|
59
|
+
summary: A simple wrapper library for Slack's Socket Mode API
|
|
60
|
+
test_files: []
|