slack-ruby-socket-mode-bot 0.1.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/.rspec +3 -0
- data/.rubocop.yml +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +15 -0
- data/Rakefile +12 -0
- data/lib/bot/bot.rb +80 -0
- data/lib/errors/errors.rb +7 -0
- data/lib/slack-ruby-socket-mode-bot.rb +13 -0
- data/lib/slack_ruby_socket_mode_bot.rb +3 -0
- data/lib/transport/socket_mode_client.rb +43 -0
- data/lib/transport/websocket.rb +110 -0
- data/lib/version.rb +5 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3d9b476cbef5846c666a7ab994c514f998f870d2004b1d2b0f3ff6c053e4c8f0
|
4
|
+
data.tar.gz: 46d7af96360646c78325592dcf4e5d4ba26ec5f6f776948e387f1d907eb0b96b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c696e7a8826e208174474dbcdfa03a242e8828a9de4b50e1add4ee3604180bc5080dc0ed45058f7978a2dcf494c7ac96d031f32c65aba702c4d283c5b2aad3e4
|
7
|
+
data.tar.gz: a60f162cf935ede9f7f08474ec8a3401b3493843207855611acf60b46965f886ec0cb2fb169206a17daa7f6ba353af131a72a3eaf3f1353ee6bfde5bd3c5cf30
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 guille
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# slack-ruby-socket-mode-bot
|
2
|
+
|
3
|
+
This gem allows users to create Slack bots that respond to mentions. This gem only supports events-based [socket mode](https://api.slack.com/apis/socket-mode) bots. The gem allows registering a number of callbacks that will be executed if the registered regular expression matches the mention text.
|
4
|
+
|
5
|
+
See the [examples](https://github.com/guille/slack-socket-mode-bot/blob/master/examples) directory for some ideas on how to use the gem.
|
6
|
+
|
7
|
+
## Limitations
|
8
|
+
|
9
|
+
- The bot assumes it is only running for one workspace.
|
10
|
+
- It only supports mention events. Other types of events such as slash commands may be implemented in the future.
|
11
|
+
- Put this together in a few hours, very likely to go boom unexpectedly.
|
12
|
+
|
13
|
+
## License
|
14
|
+
|
15
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/lib/bot/bot.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlackSocketModeBot
|
4
|
+
MentionHandler = Data.define(:match_regex, :callback)
|
5
|
+
|
6
|
+
class Bot
|
7
|
+
def initialize(bot_token:, app_token:, logger: nil)
|
8
|
+
@bot_token = bot_token
|
9
|
+
@logger = logger || Logger.new($stdout)
|
10
|
+
|
11
|
+
slack_web_client # init
|
12
|
+
|
13
|
+
@socket_mode_client = Transport::SocketModeClient.new(
|
14
|
+
app_token:, logger:, callback: method(:process_event)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Registers a callback for a matching regex
|
19
|
+
def on(match_regex, &block)
|
20
|
+
(@handlers ||= []) << MentionHandler.new(match_regex, block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def run!
|
24
|
+
@socket_mode_client.run!
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def slack_web_client
|
30
|
+
client = Slack::Web::Client.new(
|
31
|
+
token: @bot_token,
|
32
|
+
logger: @logger
|
33
|
+
)
|
34
|
+
auth_response = client.auth_test
|
35
|
+
|
36
|
+
raise auth_response unless auth_response[:ok]
|
37
|
+
|
38
|
+
@bot_user_id = auth_response[:user_id]
|
39
|
+
@team_id = auth_response[:team_id]
|
40
|
+
|
41
|
+
@slack_web_client ||= client
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle_error(err)
|
45
|
+
@logger.error(err.message)
|
46
|
+
@logger.debug(err.backtrace)
|
47
|
+
end
|
48
|
+
|
49
|
+
def process_event(payload)
|
50
|
+
return unless payload_processable?(payload)
|
51
|
+
|
52
|
+
text_without_mention = payload[:event][:text].gsub(/<@#{@bot_user_id}>\s+/, "")
|
53
|
+
|
54
|
+
@handlers.each do |handler|
|
55
|
+
process_handler(handler, text_without_mention, payload[:event])
|
56
|
+
end
|
57
|
+
rescue StandardError => e
|
58
|
+
# Don't bring down the bot from a problematic message
|
59
|
+
handle_error(e)
|
60
|
+
end
|
61
|
+
|
62
|
+
def payload_processable?(payload)
|
63
|
+
if payload[:team_id] != @team_id
|
64
|
+
raise UnrecognisedWorkspace.new, "Unrecognised team id #{payload[:team_id]} (expected #{@team_id})"
|
65
|
+
end
|
66
|
+
|
67
|
+
payload[:event][:type] == "app_mention"
|
68
|
+
end
|
69
|
+
|
70
|
+
def process_handler(handler, text, event)
|
71
|
+
if (match_data = handler.match_regex.match(text))
|
72
|
+
@logger.debug("Found matching handler for #{match_data}")
|
73
|
+
handler.callback.call(event, match_data, slack_web_client)
|
74
|
+
end
|
75
|
+
rescue StandardError => e
|
76
|
+
# Don't stop processing handlers if one of them fails
|
77
|
+
handle_error(e)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "async"
|
4
|
+
require "async/http/endpoint"
|
5
|
+
require "async/websocket"
|
6
|
+
require "logger"
|
7
|
+
require "slack-ruby-client"
|
8
|
+
|
9
|
+
require_relative "bot/bot"
|
10
|
+
require_relative "errors/errors"
|
11
|
+
require_relative "transport/socket_mode_client"
|
12
|
+
require_relative "transport/websocket"
|
13
|
+
require_relative "version"
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlackSocketModeBot
|
4
|
+
module Transport
|
5
|
+
class SocketModeClient
|
6
|
+
def initialize(app_token:, callback:, logger: nil)
|
7
|
+
@app_token = app_token
|
8
|
+
@callback = callback
|
9
|
+
@logger = logger || Logger.new($stdout)
|
10
|
+
|
11
|
+
@socket = WebSocket.new(client: self, logger: @logger)
|
12
|
+
end
|
13
|
+
|
14
|
+
def run!
|
15
|
+
@socket.connect!
|
16
|
+
end
|
17
|
+
|
18
|
+
def new_socket_url
|
19
|
+
response = slack_web_client.apps_connections_open
|
20
|
+
|
21
|
+
raise response unless response[:ok]
|
22
|
+
|
23
|
+
response[:url]
|
24
|
+
# debug only:
|
25
|
+
"#{response[:url]}&debug_reconnects=true"
|
26
|
+
end
|
27
|
+
|
28
|
+
def callback(*args)
|
29
|
+
@callback.call(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def slack_web_client
|
35
|
+
client = Slack::Web::Client.new(
|
36
|
+
token: @app_token,
|
37
|
+
logger: @logger
|
38
|
+
)
|
39
|
+
@slack_web_client ||= client
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SlackSocketModeBot
|
4
|
+
module Transport
|
5
|
+
class WebSocket
|
6
|
+
attr_reader :client
|
7
|
+
|
8
|
+
def initialize(client:, logger: nil)
|
9
|
+
@client = client
|
10
|
+
@logger = logger || Logger.new($stdout)
|
11
|
+
|
12
|
+
@restart = Async::Notification.new
|
13
|
+
@ping_id = 1
|
14
|
+
end
|
15
|
+
|
16
|
+
def connect!
|
17
|
+
Async do |task|
|
18
|
+
trap_sigterm(task)
|
19
|
+
|
20
|
+
loop do
|
21
|
+
endpoint = Async::HTTP::Endpoint.parse(client.new_socket_url)
|
22
|
+
|
23
|
+
Async::WebSocket::Client.connect(endpoint) do |connection|
|
24
|
+
@ping_task = task.async do |subtask|
|
25
|
+
subtask.annotate "socket keep-alive"
|
26
|
+
|
27
|
+
loop do
|
28
|
+
subtask.sleep 50
|
29
|
+
ping!(connection) if @restart
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
@socket_task&.stop
|
34
|
+
@socket_task = task.async do |subtask|
|
35
|
+
subtask.annotate "socket message loop"
|
36
|
+
|
37
|
+
message_loop(connection)
|
38
|
+
rescue StandardError => e
|
39
|
+
@logger.info("Message read failed: #{e.message}. Restarting the socket")
|
40
|
+
restart!
|
41
|
+
end
|
42
|
+
|
43
|
+
# Wait here letting it ping & process messages until we need to reconnect
|
44
|
+
@restart.wait
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
rescue Interrupt
|
49
|
+
puts "Interrupt detected. Exiting..."
|
50
|
+
ensure
|
51
|
+
@ping_task&.stop
|
52
|
+
@socket_task&.stop
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def message_loop(connection)
|
58
|
+
while (message = connection.read)
|
59
|
+
parsed = JSON.parse(message, symbolize_names: true)
|
60
|
+
|
61
|
+
@logger.debug("Got #{parsed.dig(:payload, :event, :type) || parsed}")
|
62
|
+
|
63
|
+
# Acknowledge all events
|
64
|
+
ack_event(connection, parsed[:envelope_id]) if parsed[:envelope_id]
|
65
|
+
|
66
|
+
# Restart when we get the warning to minimise downtime
|
67
|
+
restart! if parsed[:type] == "disconnect"
|
68
|
+
|
69
|
+
client.callback(parsed[:payload]) if parsed[:payload]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def restart!
|
74
|
+
@ping_task&.stop
|
75
|
+
@ping_id = 1
|
76
|
+
|
77
|
+
@restart.signal
|
78
|
+
end
|
79
|
+
|
80
|
+
def ping!(connection)
|
81
|
+
@logger.debug("Sending ping #{@ping_id}")
|
82
|
+
connection.send_ping("id=#{@ping_id}")
|
83
|
+
connection.flush
|
84
|
+
|
85
|
+
@ping_id += 1
|
86
|
+
rescue StandardError => e
|
87
|
+
@logger.info("Ping failed: #{e.message}. Restarting the socket")
|
88
|
+
restart!
|
89
|
+
end
|
90
|
+
|
91
|
+
def ack_event(connection, envelope_id)
|
92
|
+
connection.write(
|
93
|
+
Protocol::WebSocket::TextMessage.generate({ envelope_id: envelope_id })
|
94
|
+
)
|
95
|
+
connection.flush
|
96
|
+
end
|
97
|
+
|
98
|
+
def trap_sigterm(task)
|
99
|
+
Signal.trap("SIGTERM") do
|
100
|
+
puts("Received SIGTERM. Exiting...") # can't log from signal handler
|
101
|
+
@ping_task&.stop
|
102
|
+
@socket_task&.stop
|
103
|
+
task&.stop
|
104
|
+
end
|
105
|
+
rescue StandardError => e
|
106
|
+
puts("Error processing SIGTERM handler: #{e.message}")
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: slack-ruby-socket-mode-bot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- guille
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: async
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: async-websocket
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: slack-ruby-client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: This gem allows users to create Slack bots that respond to mentions.
|
56
|
+
This gem only supports events-based socket mode bots. The gem allows registering
|
57
|
+
a number of callbacks that will be executed if the registered regular expression
|
58
|
+
matches the mention text.
|
59
|
+
email:
|
60
|
+
- guille@users.noreply.github.com
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- ".rspec"
|
66
|
+
- ".rubocop.yml"
|
67
|
+
- LICENSE.txt
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- lib/bot/bot.rb
|
71
|
+
- lib/errors/errors.rb
|
72
|
+
- lib/slack-ruby-socket-mode-bot.rb
|
73
|
+
- lib/slack_ruby_socket_mode_bot.rb
|
74
|
+
- lib/transport/socket_mode_client.rb
|
75
|
+
- lib/transport/websocket.rb
|
76
|
+
- lib/version.rb
|
77
|
+
homepage: https://github.com/guille/slack-socket-mode-bot
|
78
|
+
licenses:
|
79
|
+
- MIT
|
80
|
+
metadata:
|
81
|
+
homepage_uri: https://github.com/guille/slack-socket-mode-bot
|
82
|
+
source_code_uri: https://github.com/guille/slack-socket-mode-bot
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: 3.0.0
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
requirements: []
|
98
|
+
rubygems_version: 3.5.3
|
99
|
+
signing_key:
|
100
|
+
specification_version: 4
|
101
|
+
summary: Gem for implementing simple bots for Slack using Socket Mode
|
102
|
+
test_files: []
|