chat_sdk-slack 1.1.0 → 1.3.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 +4 -4
- data/lib/chat_sdk/slack/adapter.rb +387 -51
- data/lib/chat_sdk/slack/block_kit_renderer.rb +118 -14
- data/lib/chat_sdk/slack/chat_streamer.rb +124 -0
- data/lib/chat_sdk/slack/event_parser.rb +86 -2
- data/lib/chat_sdk/slack/format_converter.rb +17 -0
- data/lib/chat_sdk/slack/modal_renderer.rb +56 -0
- data/lib/chat_sdk/slack/native_stream.rb +486 -0
- data/lib/chat_sdk/slack/socket_mode.rb +1 -1
- data/lib/chat_sdk/slack/token_encryption.rb +74 -0
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d10492534a772bad66a756eabd3e3a89ef35f63ed61cdb84431979be6b844906
|
|
4
|
+
data.tar.gz: 720e5ea6b813a557335e59d2c2b42c9f5194a7f2dc8dfca0c6a7e937f36cbe2c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e4bd3615d525bf00f61885c3ff8830553e99644f4a2ba319ab3c547f611b87bb6035edb2eb1bc717b11102fa1454f6fe60d1c9c8f4f824156de09f516f20858
|
|
7
|
+
data.tar.gz: 6b33b090da4111d30602550197797a2169125caa3982e7ce22b44091b9cb5de38b4a22caf6090ca4aecf448314cb6017bcbe2115182ea67dec64c7ea2141be90
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "stringio"
|
|
4
|
+
|
|
3
5
|
module ChatSDK
|
|
4
6
|
module Slack
|
|
5
7
|
class Adapter < ChatSDK::Adapter::Base
|
|
@@ -9,33 +11,86 @@ module ChatSDK
|
|
|
9
11
|
:scheduled_messages
|
|
10
12
|
|
|
11
13
|
def initialize(bot_token: nil, signing_secret: nil,
|
|
12
|
-
client_id: nil, client_secret: nil, proxy: nil
|
|
13
|
-
|
|
14
|
+
client_id: nil, client_secret: nil, proxy: nil, bot_user_id: nil,
|
|
15
|
+
installation_provider: nil, encryption_key: nil, webhook_verifier: nil,
|
|
16
|
+
native_streaming: true, stream_segment_max_age: 240)
|
|
17
|
+
@bot_token = bot_token.nil? ? ENV["SLACK_BOT_TOKEN"] : bot_token
|
|
14
18
|
@signing_secret = signing_secret || ENV["SLACK_SIGNING_SECRET"]
|
|
15
19
|
@client_id = client_id || ENV["SLACK_CLIENT_ID"]
|
|
16
20
|
@client_secret = client_secret || ENV["SLACK_CLIENT_SECRET"]
|
|
21
|
+
@bot_user_id = bot_user_id || ENV["SLACK_BOT_USER_ID"]
|
|
22
|
+
@installation_provider = installation_provider
|
|
23
|
+
@webhook_verifier = webhook_verifier
|
|
24
|
+
encryption_key ||= ENV["SLACK_ENCRYPTION_KEY"]
|
|
25
|
+
@encryption_key = TokenEncryption.decode_key(encryption_key) if encryption_key
|
|
26
|
+
valid_segment_max_age = stream_segment_max_age.is_a?(Numeric) &&
|
|
27
|
+
!stream_segment_max_age.is_a?(Complex) &&
|
|
28
|
+
!(stream_segment_max_age.respond_to?(:nan?) && stream_segment_max_age.nan?) &&
|
|
29
|
+
(stream_segment_max_age.positive? || stream_segment_max_age == Float::INFINITY)
|
|
30
|
+
unless valid_segment_max_age
|
|
31
|
+
raise ChatSDK::ConfigurationError, "stream_segment_max_age must be a positive number"
|
|
32
|
+
end
|
|
33
|
+
@native_streaming = native_streaming
|
|
34
|
+
@native_streaming_broken = false
|
|
35
|
+
@stream_segment_max_age = stream_segment_max_age
|
|
36
|
+
@bot_user_ids = {}
|
|
37
|
+
@bot_user_id_mutex = Mutex.new
|
|
38
|
+
@client_cache = {}
|
|
39
|
+
@client_cache_mutex = Mutex.new
|
|
40
|
+
@context_key = :"chat_sdk_slack_context_#{object_id}"
|
|
17
41
|
@proxy = proxy || ENV["HTTPS_PROXY"] || ENV["https_proxy"]
|
|
18
42
|
|
|
19
|
-
|
|
20
|
-
raise ChatSDK::ConfigurationError, "Slack bot_token or
|
|
43
|
+
if !@bot_token.nil? && !@bot_token.is_a?(String) && !@bot_token.respond_to?(:call)
|
|
44
|
+
raise ChatSDK::ConfigurationError, "Slack bot_token must be a String or callable"
|
|
45
|
+
end
|
|
46
|
+
if !@installation_provider.nil? && !@installation_provider.respond_to?(:get_installation) && !@installation_provider.respond_to?(:call)
|
|
47
|
+
raise ChatSDK::ConfigurationError, "Slack installation_provider must respond to get_installation or call"
|
|
48
|
+
end
|
|
49
|
+
if !@webhook_verifier.nil? && !@webhook_verifier.respond_to?(:call)
|
|
50
|
+
raise ChatSDK::ConfigurationError, "Slack webhook_verifier must be callable"
|
|
51
|
+
end
|
|
52
|
+
unless @bot_token || @client_id || @installation_provider
|
|
53
|
+
raise ChatSDK::ConfigurationError, "Slack bot_token, client_id or installation_provider required"
|
|
54
|
+
end
|
|
55
|
+
if @webhook_verifier.nil? && !@signing_secret
|
|
56
|
+
raise ChatSDK::ConfigurationError, "Slack signing_secret or webhook_verifier required"
|
|
21
57
|
end
|
|
22
|
-
raise ChatSDK::ConfigurationError, "Slack signing_secret required" unless @signing_secret
|
|
23
58
|
|
|
24
|
-
if @bot_token
|
|
59
|
+
if @bot_token.is_a?(String)
|
|
25
60
|
::Slack.configure do |config|
|
|
26
61
|
config.token = @bot_token
|
|
27
62
|
end
|
|
28
|
-
@client =
|
|
63
|
+
@client = client_for_token(@bot_token)
|
|
29
64
|
end
|
|
30
65
|
|
|
31
66
|
@renderer = BlockKitRenderer.new
|
|
32
67
|
@modal_renderer = ModalRenderer.new
|
|
33
|
-
@team_clients = {}
|
|
34
68
|
end
|
|
35
69
|
|
|
36
70
|
# Returns the per-request client (multi-workspace) or the static client (single-workspace).
|
|
37
71
|
def client
|
|
38
|
-
|
|
72
|
+
request_context = context
|
|
73
|
+
return client_for_token(request_context[:token]) if request_context && !request_context[:token].nil?
|
|
74
|
+
return @client if @bot_token.is_a?(String)
|
|
75
|
+
return unless @bot_token.respond_to?(:call)
|
|
76
|
+
|
|
77
|
+
token = @bot_token.call
|
|
78
|
+
if token.nil? || token == false || (token.respond_to?(:empty?) && token.empty?)
|
|
79
|
+
raise ChatSDK::ConfigurationError, "Slack bot_token resolver returned no token"
|
|
80
|
+
end
|
|
81
|
+
client_for_token(token)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def multi_workspace?
|
|
85
|
+
@bot_token.nil? || !@client_id.nil?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def with_bot_token(token, installation_id: nil)
|
|
89
|
+
previous_context = context
|
|
90
|
+
set_context(token: token, installation_id: installation_id)
|
|
91
|
+
yield
|
|
92
|
+
ensure
|
|
93
|
+
set_context(previous_context)
|
|
39
94
|
end
|
|
40
95
|
|
|
41
96
|
# Inject state store after initialization (e.g., from Chat instance).
|
|
@@ -45,25 +100,39 @@ module ChatSDK
|
|
|
45
100
|
|
|
46
101
|
# --- Multi-workspace installation management ---
|
|
47
102
|
|
|
48
|
-
def set_installation(team_id, bot_token:, bot_user_id: nil, team_name: nil
|
|
103
|
+
def set_installation(team_id, bot_token:, bot_user_id: nil, team_name: nil,
|
|
104
|
+
enterprise_id: nil, enterprise_install: nil)
|
|
49
105
|
raise ChatSDK::ConfigurationError, "Multi-workspace mode requires state" unless @state
|
|
50
106
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"bot_token" => bot_token,
|
|
107
|
+
installation = {
|
|
108
|
+
"bot_token" => @encryption_key ? TokenEncryption.encrypt(bot_token, @encryption_key) : bot_token,
|
|
54
109
|
"bot_user_id" => bot_user_id,
|
|
55
110
|
"team_name" => team_name
|
|
56
|
-
}
|
|
111
|
+
}
|
|
112
|
+
installation["enterprise_id"] = enterprise_id unless enterprise_id.nil?
|
|
113
|
+
installation["is_enterprise_install"] = enterprise_install unless enterprise_install.nil?
|
|
114
|
+
@state.set(installation_key(team_id), installation)
|
|
115
|
+
invalidate_bot_user_id(team_id)
|
|
57
116
|
end
|
|
58
117
|
|
|
59
118
|
def get_installation(team_id)
|
|
60
119
|
return nil unless @state
|
|
61
120
|
|
|
62
|
-
@state.get(installation_key(team_id))
|
|
121
|
+
installation = @state.get(installation_key(team_id))
|
|
122
|
+
return installation unless installation.is_a?(Hash)
|
|
123
|
+
|
|
124
|
+
token_key = installation.key?("bot_token") ? "bot_token" : :bot_token
|
|
125
|
+
token = installation[token_key]
|
|
126
|
+
return installation unless TokenEncryption.encrypted?(token)
|
|
127
|
+
unless @encryption_key
|
|
128
|
+
raise ChatSDK::ConfigurationError, "Slack installation token is encrypted; configure encryption_key"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
installation.merge(token_key => TokenEncryption.decrypt(token, @encryption_key))
|
|
63
132
|
end
|
|
64
133
|
|
|
65
134
|
def delete_installation(team_id)
|
|
66
|
-
|
|
135
|
+
invalidate_bot_user_id(team_id)
|
|
67
136
|
return unless @state
|
|
68
137
|
|
|
69
138
|
@state.delete(installation_key(team_id))
|
|
@@ -82,19 +151,39 @@ module ChatSDK
|
|
|
82
151
|
|
|
83
152
|
result = temp_client.oauth_v2_access(**params)
|
|
84
153
|
|
|
85
|
-
|
|
154
|
+
team = hash_value(result, "team")
|
|
155
|
+
enterprise = hash_value(result, "enterprise")
|
|
156
|
+
enterprise_install = truthy_slack_value?(hash_value(result, "is_enterprise_install"))
|
|
157
|
+
enterprise_id = hash_value(enterprise, "id") || hash_value(result, "enterprise_id")
|
|
158
|
+
team_id = hash_value(team, "id")
|
|
159
|
+
installation_id = enterprise_install ? enterprise_id : team_id
|
|
160
|
+
access_token = hash_value(result, "access_token")
|
|
161
|
+
unless access_token && !access_token.empty? && installation_id && !installation_id.empty?
|
|
162
|
+
missing = enterprise_install ? "access_token or enterprise.id" : "access_token or team.id"
|
|
163
|
+
raise ChatSDK::ConfigurationError, "Slack OAuth response missing #{missing}"
|
|
164
|
+
end
|
|
165
|
+
|
|
86
166
|
installation = {
|
|
87
|
-
"bot_token" =>
|
|
88
|
-
"bot_user_id" => result
|
|
89
|
-
"team_name" =>
|
|
167
|
+
"bot_token" => access_token,
|
|
168
|
+
"bot_user_id" => hash_value(result, "bot_user_id"),
|
|
169
|
+
"team_name" => hash_value(team, "name") || hash_value(enterprise, "name")
|
|
90
170
|
}
|
|
171
|
+
installation["enterprise_id"] = enterprise_id unless enterprise_id.nil?
|
|
172
|
+
installation["is_enterprise_install"] = enterprise_install if hash_key?(result, "is_enterprise_install")
|
|
91
173
|
|
|
92
|
-
set_installation(
|
|
174
|
+
set_installation(installation_id,
|
|
93
175
|
bot_token: installation["bot_token"],
|
|
94
176
|
bot_user_id: installation["bot_user_id"],
|
|
95
|
-
team_name: installation["team_name"]
|
|
96
|
-
|
|
97
|
-
|
|
177
|
+
team_name: installation["team_name"],
|
|
178
|
+
enterprise_id: enterprise_id,
|
|
179
|
+
enterprise_install: hash_key?(result, "is_enterprise_install") ? enterprise_install : nil)
|
|
180
|
+
|
|
181
|
+
{
|
|
182
|
+
team_id: installation_id,
|
|
183
|
+
enterprise_id: enterprise_id,
|
|
184
|
+
enterprise_install: enterprise_install,
|
|
185
|
+
installation: installation
|
|
186
|
+
}
|
|
98
187
|
end
|
|
99
188
|
|
|
100
189
|
def name
|
|
@@ -105,6 +194,18 @@ module ChatSDK
|
|
|
105
194
|
def verify_request!(rack_request)
|
|
106
195
|
body = rack_request.body.read
|
|
107
196
|
rack_request.body.rewind
|
|
197
|
+
if @webhook_verifier
|
|
198
|
+
begin
|
|
199
|
+
result = @webhook_verifier.call(rack_request, body)
|
|
200
|
+
rescue => e
|
|
201
|
+
raise ChatSDK::SignatureVerificationError, "Slack webhook verifier failed: #{e.message}"
|
|
202
|
+
end
|
|
203
|
+
raise ChatSDK::SignatureVerificationError, "Slack webhook verifier rejected the request" unless result
|
|
204
|
+
|
|
205
|
+
rack_request.env["rack.input"] = StringIO.new(result) if result.is_a?(String)
|
|
206
|
+
return true
|
|
207
|
+
end
|
|
208
|
+
|
|
108
209
|
timestamp = rack_request.env["HTTP_X_SLACK_REQUEST_TIMESTAMP"]
|
|
109
210
|
signature = rack_request.env["HTTP_X_SLACK_SIGNATURE"]
|
|
110
211
|
|
|
@@ -139,21 +240,17 @@ module ChatSDK
|
|
|
139
240
|
end
|
|
140
241
|
|
|
141
242
|
def parse_events(rack_request)
|
|
243
|
+
set_context(nil)
|
|
142
244
|
body = rack_request.body.read
|
|
143
245
|
rack_request.body.rewind
|
|
144
246
|
|
|
145
247
|
parsed = parse_body(body, rack_request.content_type)
|
|
146
248
|
return [] unless parsed
|
|
147
249
|
|
|
148
|
-
|
|
149
|
-
# Clear any stale client from a previous request on this thread (Puma thread pool safety)
|
|
150
|
-
if @client_id
|
|
151
|
-
::Thread.current[:chat_sdk_slack_client] = nil
|
|
152
|
-
team_id = parsed["team_id"] || parsed.dig("team", "id")
|
|
153
|
-
resolve_team_client(team_id) if team_id
|
|
154
|
-
end
|
|
250
|
+
return [] unless prepare_payload_context(parsed)
|
|
155
251
|
|
|
156
|
-
EventParser.parse(parsed)
|
|
252
|
+
events = EventParser.parse(parsed)
|
|
253
|
+
classify_message_events(events, parsed)
|
|
157
254
|
end
|
|
158
255
|
|
|
159
256
|
# Outbound
|
|
@@ -177,7 +274,27 @@ module ChatSDK
|
|
|
177
274
|
)
|
|
178
275
|
end
|
|
179
276
|
|
|
180
|
-
def
|
|
277
|
+
def stream(channel_id:, thread_id:, stream:, options: {})
|
|
278
|
+
return unless thread_id && !thread_id.to_s.empty?
|
|
279
|
+
direct_message = channel_id.to_s.start_with?("D")
|
|
280
|
+
recipient_ids = [options[:recipient_user_id], options[:recipient_team_id]]
|
|
281
|
+
has_recipient_context = recipient_ids.all? { |id| id && !id.to_s.empty? }
|
|
282
|
+
return unless direct_message || has_recipient_context
|
|
283
|
+
return unless @native_streaming && !@native_streaming_broken
|
|
284
|
+
|
|
285
|
+
stream_client = client
|
|
286
|
+
NativeStream.new(
|
|
287
|
+
adapter: self,
|
|
288
|
+
client: stream_client,
|
|
289
|
+
channel_id: channel_id,
|
|
290
|
+
thread_id: thread_id,
|
|
291
|
+
stream: stream,
|
|
292
|
+
options: options,
|
|
293
|
+
max_age: @stream_segment_max_age
|
|
294
|
+
).run
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def edit_message(channel_id:, message_id:, message:, **_options)
|
|
181
298
|
msg = ChatSDK::PostableMessage.from(message)
|
|
182
299
|
params = {channel: channel_id, ts: message_id}
|
|
183
300
|
|
|
@@ -186,7 +303,7 @@ module ChatSDK
|
|
|
186
303
|
client.chat_update(**params)
|
|
187
304
|
end
|
|
188
305
|
|
|
189
|
-
def delete_message(channel_id:, message_id
|
|
306
|
+
def delete_message(channel_id:, message_id:, **_options)
|
|
190
307
|
client.chat_delete(channel: channel_id, ts: message_id)
|
|
191
308
|
end
|
|
192
309
|
|
|
@@ -201,17 +318,19 @@ module ChatSDK
|
|
|
201
318
|
end
|
|
202
319
|
|
|
203
320
|
def upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil)
|
|
204
|
-
|
|
321
|
+
content = io.read
|
|
322
|
+
io.rewind if io.respond_to?(:rewind)
|
|
323
|
+
params = {channel_id: channel_id, filename: filename, content: content}
|
|
205
324
|
params[:thread_ts] = thread_id if thread_id
|
|
206
325
|
params[:initial_comment] = comment if comment
|
|
207
|
-
client.
|
|
326
|
+
client.files_upload_v2(**params)
|
|
208
327
|
end
|
|
209
328
|
|
|
210
|
-
def add_reaction(channel_id:, message_id:, emoji
|
|
329
|
+
def add_reaction(channel_id:, message_id:, emoji:, **_options)
|
|
211
330
|
client.reactions_add(channel: channel_id, timestamp: message_id, name: emoji)
|
|
212
331
|
end
|
|
213
332
|
|
|
214
|
-
def remove_reaction(channel_id:, message_id:, emoji
|
|
333
|
+
def remove_reaction(channel_id:, message_id:, emoji:, **_options)
|
|
215
334
|
client.reactions_remove(channel: channel_id, timestamp: message_id, name: emoji)
|
|
216
335
|
end
|
|
217
336
|
|
|
@@ -315,11 +434,24 @@ module ChatSDK
|
|
|
315
434
|
result["channel"]
|
|
316
435
|
end
|
|
317
436
|
|
|
318
|
-
def start_typing(channel_id:, thread_id: nil)
|
|
437
|
+
def start_typing(channel_id:, thread_id: nil, message_id: nil)
|
|
319
438
|
# Slack doesn't have a native typing indicator API for bots
|
|
320
439
|
# This is a no-op but the capability is declared for streaming support
|
|
321
440
|
end
|
|
322
441
|
|
|
442
|
+
def concurrency_scope(event)
|
|
443
|
+
return context[:installation_id] if context && context[:installation_id]
|
|
444
|
+
return unless multi_workspace?
|
|
445
|
+
|
|
446
|
+
raw = event.raw.is_a?(Hash) ? event.raw : {}
|
|
447
|
+
raw_event = raw.is_a?(Hash) ? raw["event"] || raw[:event] || raw : {}
|
|
448
|
+
raw_team = raw.is_a?(Hash) ? raw["team"] || raw[:team] : nil
|
|
449
|
+
raw_team_id = raw_team.is_a?(Hash) ? raw_team["id"] || raw_team[:id] : raw_team
|
|
450
|
+
raw_team_id || raw["team_id"] || raw[:team_id] ||
|
|
451
|
+
raw_event["team_id"] || raw_event[:team_id] ||
|
|
452
|
+
raw_event["team"] || raw_event[:team]
|
|
453
|
+
end
|
|
454
|
+
|
|
323
455
|
# Slack-specific: receive real-time events via Socket Mode WebSocket.
|
|
324
456
|
# Requires the optional 'faye-websocket' gem and an app-level token (xapp-*).
|
|
325
457
|
# Not part of the base adapter contract.
|
|
@@ -331,9 +463,17 @@ module ChatSDK
|
|
|
331
463
|
def start_socket_mode(app_token: nil, &block)
|
|
332
464
|
app_token ||= ENV["SLACK_APP_TOKEN"]
|
|
333
465
|
raise ChatSDK::ConfigurationError, "Slack app_token required for socket mode" unless app_token
|
|
466
|
+
raise ArgumentError, "start requires a block" unless block
|
|
334
467
|
|
|
335
468
|
@socket_mode = SocketMode.new(app_token: app_token, bot_client: client)
|
|
336
|
-
@socket_mode.start
|
|
469
|
+
@socket_mode.start do |event, payload|
|
|
470
|
+
payload ||= event.raw
|
|
471
|
+
set_context(nil)
|
|
472
|
+
next unless prepare_payload_context(payload)
|
|
473
|
+
|
|
474
|
+
classify_message_events([event], payload)
|
|
475
|
+
block.call(event)
|
|
476
|
+
end
|
|
337
477
|
end
|
|
338
478
|
|
|
339
479
|
def stop_socket_mode
|
|
@@ -354,28 +494,224 @@ module ChatSDK
|
|
|
354
494
|
|
|
355
495
|
private
|
|
356
496
|
|
|
497
|
+
def monotonic_now
|
|
498
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
499
|
+
end
|
|
500
|
+
|
|
357
501
|
def build_client(token: nil)
|
|
358
502
|
options = {}
|
|
359
|
-
options[:token] = token
|
|
503
|
+
options[:token] = token unless token.nil?
|
|
360
504
|
options[:proxy] = @proxy if @proxy
|
|
361
505
|
options.empty? ? ::Slack::Web::Client.new : ::Slack::Web::Client.new(options)
|
|
362
506
|
end
|
|
363
507
|
|
|
364
|
-
def
|
|
365
|
-
|
|
508
|
+
def classify_message_events(events, payload)
|
|
509
|
+
events.each do |event|
|
|
510
|
+
next unless %i[mention subscribed_message direct_message].include?(event.type)
|
|
511
|
+
|
|
512
|
+
event.message.is_mention = classify_mention(event, payload)
|
|
513
|
+
end
|
|
514
|
+
events
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def classify_mention(event, payload)
|
|
518
|
+
raw_event = payload["event"] || {}
|
|
519
|
+
return true if raw_event["type"] == "app_mention"
|
|
520
|
+
|
|
521
|
+
bot_user_id = resolve_bot_user_id(event)
|
|
522
|
+
return true if event.type == :mention && bot_user_id.nil?
|
|
523
|
+
return nil if bot_user_id.nil?
|
|
524
|
+
|
|
525
|
+
text = raw_event["text"].to_s
|
|
526
|
+
masked_text = ChatSDK::Format::Mentions.mask_code_spans(text)
|
|
527
|
+
text_mentions_bot = /<@#{Regexp.escape(bot_user_id)}(?:\|[^>]*)?>/.match?(masked_text)
|
|
528
|
+
text_mentions_bot || rich_text_mentions_bot?(raw_event["blocks"], bot_user_id)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def resolve_bot_user_id(event)
|
|
532
|
+
request_context = context
|
|
533
|
+
return request_context[:bot_user_id] if request_context && request_context[:bot_user_id]
|
|
534
|
+
|
|
535
|
+
return @bot_user_id if @bot_user_id
|
|
536
|
+
|
|
537
|
+
installation_id = if multi_workspace?
|
|
538
|
+
request_context&.[](:installation_id) || event_installation_id(event)
|
|
539
|
+
end
|
|
540
|
+
installation = resolve_installation(installation_id, enterprise_install: installation_enterprise_flag(event)) if installation_id && !request_context
|
|
541
|
+
installation_bot_user_id = hash_value(installation, "bot_user_id")
|
|
542
|
+
return installation_bot_user_id if installation_bot_user_id
|
|
543
|
+
|
|
544
|
+
token = request_context&.[](:token) || hash_value(installation, "bot_token")
|
|
545
|
+
current_client = token ? client_for_token(token) : client
|
|
546
|
+
return unless current_client
|
|
547
|
+
|
|
548
|
+
@bot_user_id_mutex.synchronize do
|
|
549
|
+
cache_key = multi_workspace? ? (installation_id || :default) : :default
|
|
550
|
+
return @bot_user_ids[cache_key] if @bot_user_ids.key?(cache_key)
|
|
551
|
+
|
|
552
|
+
bot_user_id = begin
|
|
553
|
+
current_client.auth_test["user_id"]
|
|
554
|
+
rescue ::Slack::Web::Api::Errors::SlackError, Faraday::Error, SocketError, Timeout::Error
|
|
555
|
+
nil
|
|
556
|
+
end
|
|
557
|
+
@bot_user_ids[cache_key] = bot_user_id if bot_user_id
|
|
558
|
+
bot_user_id
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def invalidate_bot_user_id(team_id)
|
|
563
|
+
@bot_user_id_mutex.synchronize { @bot_user_ids.delete(team_id) }
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def rich_text_mentions_bot?(blocks, bot_user_id)
|
|
567
|
+
Array(blocks).any? { |block| rich_text_element_mentions_bot?(block, bot_user_id) }
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def rich_text_element_mentions_bot?(element, bot_user_id)
|
|
571
|
+
return false unless element.is_a?(Hash)
|
|
572
|
+
return false if %w[rich_text_preformatted preformatted code].include?(element["type"])
|
|
573
|
+
return false if element.dig("style", "code")
|
|
574
|
+
return true if element["type"] == "user" && element["user_id"] == bot_user_id
|
|
575
|
+
|
|
576
|
+
Array(element["elements"]).any? { |child| rich_text_element_mentions_bot?(child, bot_user_id) }
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def client_for_token(token)
|
|
580
|
+
@client_cache_mutex.synchronize do
|
|
581
|
+
return @client_cache[token] if @client_cache.key?(token)
|
|
582
|
+
|
|
583
|
+
@client_cache[token] = build_client(token: token)
|
|
584
|
+
@client_cache.shift while @client_cache.size > 256
|
|
585
|
+
@client_cache[token]
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def context
|
|
590
|
+
::Thread.current[@context_key]
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def set_context(value)
|
|
594
|
+
::Thread.current[@context_key] = value
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def prepare_payload_context(payload)
|
|
598
|
+
return true unless multi_workspace?
|
|
599
|
+
|
|
600
|
+
identity = extract_installation(payload)
|
|
601
|
+
return true unless identity
|
|
602
|
+
|
|
603
|
+
installation = resolve_installation(
|
|
604
|
+
identity[:installation_id],
|
|
605
|
+
enterprise_install: identity[:enterprise_install]
|
|
606
|
+
)
|
|
607
|
+
token = hash_value(installation, "bot_token")
|
|
608
|
+
unless installation && token && token != false && !(token.respond_to?(:empty?) && token.empty?)
|
|
609
|
+
return true unless @bot_token.nil?
|
|
610
|
+
|
|
611
|
+
ChatSDK::Log.warn("Dropping Slack event without a resolved installation for #{identity[:installation_id]}")
|
|
612
|
+
return false
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
set_context({
|
|
616
|
+
token: token,
|
|
617
|
+
bot_user_id: hash_value(installation, "bot_user_id"),
|
|
618
|
+
installation_id: identity[:installation_id],
|
|
619
|
+
enterprise_install: identity[:enterprise_install],
|
|
620
|
+
team_id: identity[:team_id],
|
|
621
|
+
enterprise_id: identity[:enterprise_id]
|
|
622
|
+
})
|
|
623
|
+
true
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def resolve_installation(installation_id, enterprise_install:)
|
|
627
|
+
return unless installation_id
|
|
366
628
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
629
|
+
installation = if @installation_provider
|
|
630
|
+
if @installation_provider.respond_to?(:get_installation)
|
|
631
|
+
@installation_provider.get_installation(installation_id, enterprise_install: enterprise_install)
|
|
632
|
+
else
|
|
633
|
+
@installation_provider.call(installation_id, enterprise_install: enterprise_install)
|
|
634
|
+
end
|
|
635
|
+
else
|
|
636
|
+
get_installation(installation_id)
|
|
637
|
+
end
|
|
638
|
+
unless installation
|
|
639
|
+
ChatSDK::Log.warn("Slack installation not found for #{installation_id}")
|
|
640
|
+
return
|
|
641
|
+
end
|
|
642
|
+
token = hash_value(installation, "bot_token")
|
|
643
|
+
unless installation.is_a?(Hash) && token && token != false && !(token.respond_to?(:empty?) && token.empty?)
|
|
644
|
+
ChatSDK::Log.warn("Slack installation for #{installation_id} has no bot_token")
|
|
370
645
|
return
|
|
371
646
|
end
|
|
372
647
|
|
|
373
|
-
installation
|
|
374
|
-
|
|
648
|
+
installation
|
|
649
|
+
rescue => e
|
|
650
|
+
ChatSDK::Log.error("Could not resolve Slack installation #{installation_id}: #{e.message}")
|
|
651
|
+
nil
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def extract_installation(payload)
|
|
655
|
+
return unless payload.is_a?(Hash)
|
|
656
|
+
|
|
657
|
+
if hash_value(payload, "type") == "event_callback"
|
|
658
|
+
authorization = Array(hash_value(payload, "authorizations")).first
|
|
659
|
+
enterprise_flag = if hash_key?(authorization, "is_enterprise_install")
|
|
660
|
+
hash_value(authorization, "is_enterprise_install")
|
|
661
|
+
else
|
|
662
|
+
hash_value(payload, "is_enterprise_install")
|
|
663
|
+
end
|
|
664
|
+
enterprise_install = truthy_slack_value?(enterprise_flag)
|
|
665
|
+
team_id = hash_value(authorization, "team_id") || hash_value(payload, "team_id")
|
|
666
|
+
enterprise_id = hash_value(authorization, "enterprise_id") || hash_value(payload, "enterprise_id")
|
|
667
|
+
elsif hash_key?(payload, "command")
|
|
668
|
+
enterprise_install = hash_value(payload, "is_enterprise_install") == "true"
|
|
669
|
+
team_id = hash_value(payload, "team_id")
|
|
670
|
+
enterprise_id = hash_value(payload, "enterprise_id")
|
|
671
|
+
else
|
|
672
|
+
enterprise_install = truthy_slack_value?(hash_value(payload, "is_enterprise_install"))
|
|
673
|
+
team = hash_value(payload, "team")
|
|
674
|
+
enterprise = hash_value(payload, "enterprise")
|
|
675
|
+
team_id = hash_value(team, "id") || hash_value(payload, "team_id")
|
|
676
|
+
enterprise_id = hash_value(enterprise, "id") || hash_value(payload, "enterprise_id")
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
installation_id = enterprise_install ? enterprise_id : team_id
|
|
680
|
+
return unless installation_id
|
|
681
|
+
|
|
682
|
+
{
|
|
683
|
+
installation_id: installation_id,
|
|
684
|
+
enterprise_install: enterprise_install,
|
|
685
|
+
team_id: team_id,
|
|
686
|
+
enterprise_id: enterprise_id
|
|
687
|
+
}
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def event_installation_id(event)
|
|
691
|
+
raw = event.raw.is_a?(Hash) ? event.raw : {}
|
|
692
|
+
extract_installation(raw)&.[](:installation_id) ||
|
|
693
|
+
hash_value(raw, "team_id") ||
|
|
694
|
+
hash_value(hash_value(raw, "team"), "id")
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def installation_enterprise_flag(event)
|
|
698
|
+
raw = event.raw.is_a?(Hash) ? event.raw : {}
|
|
699
|
+
identity = extract_installation(raw)
|
|
700
|
+
identity ? identity[:enterprise_install] : truthy_slack_value?(hash_value(raw, "is_enterprise_install"))
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def hash_value(hash, key)
|
|
704
|
+
return unless hash.is_a?(Hash)
|
|
705
|
+
|
|
706
|
+
hash.key?(key) ? hash[key] : hash[key.to_sym]
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def hash_key?(hash, key)
|
|
710
|
+
hash.is_a?(Hash) && (hash.key?(key) || hash.key?(key.to_sym))
|
|
711
|
+
end
|
|
375
712
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
::Thread.current[:chat_sdk_slack_client] = new_client
|
|
713
|
+
def truthy_slack_value?(value)
|
|
714
|
+
value == true || value == "true"
|
|
379
715
|
end
|
|
380
716
|
|
|
381
717
|
def installation_key(team_id)
|