chat_sdk-slack 1.2.0 → 1.4.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 +300 -64
- data/lib/chat_sdk/slack/block_kit_renderer.rb +57 -4
- data/lib/chat_sdk/slack/chat_streamer.rb +124 -0
- data/lib/chat_sdk/slack/event_parser.rb +53 -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: 6241d4c2d31e93f23988f0454bdc618d527d03542b1fad95fdb0326024d4268c
|
|
4
|
+
data.tar.gz: 720e5ea6b813a557335e59d2c2b42c9f5194a7f2dc8dfca0c6a7e937f36cbe2c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '08bdf2fc0b16165e5de33015cf87c40aea4b5ace2934c3cf503a0488f43ba22cce49a81aaa2aa06078a4d8dc414a0be2ac958a5d9b8c4e66701ebf62048e74e6'
|
|
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,38 +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, bot_user_id: 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"]
|
|
17
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
|
|
18
36
|
@bot_user_ids = {}
|
|
19
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}"
|
|
20
41
|
@proxy = proxy || ENV["HTTPS_PROXY"] || ENV["https_proxy"]
|
|
21
42
|
|
|
22
|
-
|
|
23
|
-
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"
|
|
24
57
|
end
|
|
25
|
-
raise ChatSDK::ConfigurationError, "Slack signing_secret required" unless @signing_secret
|
|
26
58
|
|
|
27
|
-
if @bot_token
|
|
59
|
+
if @bot_token.is_a?(String)
|
|
28
60
|
::Slack.configure do |config|
|
|
29
61
|
config.token = @bot_token
|
|
30
62
|
end
|
|
31
|
-
@client =
|
|
63
|
+
@client = client_for_token(@bot_token)
|
|
32
64
|
end
|
|
33
65
|
|
|
34
66
|
@renderer = BlockKitRenderer.new
|
|
35
67
|
@modal_renderer = ModalRenderer.new
|
|
36
|
-
@team_clients = {}
|
|
37
68
|
end
|
|
38
69
|
|
|
39
70
|
# Returns the per-request client (multi-workspace) or the static client (single-workspace).
|
|
40
71
|
def client
|
|
41
|
-
|
|
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
|
|
42
83
|
|
|
43
|
-
|
|
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)
|
|
44
94
|
end
|
|
45
95
|
|
|
46
96
|
# Inject state store after initialization (e.g., from Chat instance).
|
|
@@ -50,26 +100,38 @@ module ChatSDK
|
|
|
50
100
|
|
|
51
101
|
# --- Multi-workspace installation management ---
|
|
52
102
|
|
|
53
|
-
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)
|
|
54
105
|
raise ChatSDK::ConfigurationError, "Multi-workspace mode requires state" unless @state
|
|
55
106
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"bot_token" => bot_token,
|
|
107
|
+
installation = {
|
|
108
|
+
"bot_token" => @encryption_key ? TokenEncryption.encrypt(bot_token, @encryption_key) : bot_token,
|
|
59
109
|
"bot_user_id" => bot_user_id,
|
|
60
110
|
"team_name" => team_name
|
|
61
|
-
}
|
|
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)
|
|
62
115
|
invalidate_bot_user_id(team_id)
|
|
63
116
|
end
|
|
64
117
|
|
|
65
118
|
def get_installation(team_id)
|
|
66
119
|
return nil unless @state
|
|
67
120
|
|
|
68
|
-
@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))
|
|
69
132
|
end
|
|
70
133
|
|
|
71
134
|
def delete_installation(team_id)
|
|
72
|
-
@team_clients.delete(team_id)
|
|
73
135
|
invalidate_bot_user_id(team_id)
|
|
74
136
|
return unless @state
|
|
75
137
|
|
|
@@ -89,19 +151,39 @@ module ChatSDK
|
|
|
89
151
|
|
|
90
152
|
result = temp_client.oauth_v2_access(**params)
|
|
91
153
|
|
|
92
|
-
|
|
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
|
+
|
|
93
166
|
installation = {
|
|
94
|
-
"bot_token" =>
|
|
95
|
-
"bot_user_id" => result
|
|
96
|
-
"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")
|
|
97
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")
|
|
98
173
|
|
|
99
|
-
set_installation(
|
|
174
|
+
set_installation(installation_id,
|
|
100
175
|
bot_token: installation["bot_token"],
|
|
101
176
|
bot_user_id: installation["bot_user_id"],
|
|
102
|
-
team_name: installation["team_name"]
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|
|
105
187
|
end
|
|
106
188
|
|
|
107
189
|
def name
|
|
@@ -112,6 +194,18 @@ module ChatSDK
|
|
|
112
194
|
def verify_request!(rack_request)
|
|
113
195
|
body = rack_request.body.read
|
|
114
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
|
+
|
|
115
209
|
timestamp = rack_request.env["HTTP_X_SLACK_REQUEST_TIMESTAMP"]
|
|
116
210
|
signature = rack_request.env["HTTP_X_SLACK_SIGNATURE"]
|
|
117
211
|
|
|
@@ -146,19 +240,14 @@ module ChatSDK
|
|
|
146
240
|
end
|
|
147
241
|
|
|
148
242
|
def parse_events(rack_request)
|
|
243
|
+
set_context(nil)
|
|
149
244
|
body = rack_request.body.read
|
|
150
245
|
rack_request.body.rewind
|
|
151
246
|
|
|
152
247
|
parsed = parse_body(body, rack_request.content_type)
|
|
153
248
|
return [] unless parsed
|
|
154
249
|
|
|
155
|
-
|
|
156
|
-
# Clear any stale client from a previous request on this thread (Puma thread pool safety)
|
|
157
|
-
if @client_id
|
|
158
|
-
::Thread.current[:chat_sdk_slack_client] = nil
|
|
159
|
-
team_id = parsed["team_id"] || parsed.dig("team", "id")
|
|
160
|
-
resolve_team_client(team_id) if team_id
|
|
161
|
-
end
|
|
250
|
+
return [] unless prepare_payload_context(parsed)
|
|
162
251
|
|
|
163
252
|
events = EventParser.parse(parsed)
|
|
164
253
|
classify_message_events(events, parsed)
|
|
@@ -185,6 +274,26 @@ module ChatSDK
|
|
|
185
274
|
)
|
|
186
275
|
end
|
|
187
276
|
|
|
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
|
+
|
|
188
297
|
def edit_message(channel_id:, message_id:, message:, **_options)
|
|
189
298
|
msg = ChatSDK::PostableMessage.from(message)
|
|
190
299
|
params = {channel: channel_id, ts: message_id}
|
|
@@ -331,13 +440,16 @@ module ChatSDK
|
|
|
331
440
|
end
|
|
332
441
|
|
|
333
442
|
def concurrency_scope(event)
|
|
334
|
-
return
|
|
443
|
+
return context[:installation_id] if context && context[:installation_id]
|
|
444
|
+
return unless multi_workspace?
|
|
335
445
|
|
|
336
446
|
raw = event.raw.is_a?(Hash) ? event.raw : {}
|
|
337
447
|
raw_event = raw.is_a?(Hash) ? raw["event"] || raw[:event] || raw : {}
|
|
338
448
|
raw_team = raw.is_a?(Hash) ? raw["team"] || raw[:team] : nil
|
|
339
449
|
raw_team_id = raw_team.is_a?(Hash) ? raw_team["id"] || raw_team[:id] : raw_team
|
|
340
|
-
raw_team_id || raw["team_id"] || raw[:team_id] ||
|
|
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]
|
|
341
453
|
end
|
|
342
454
|
|
|
343
455
|
# Slack-specific: receive real-time events via Socket Mode WebSocket.
|
|
@@ -354,13 +466,12 @@ module ChatSDK
|
|
|
354
466
|
raise ArgumentError, "start requires a block" unless block
|
|
355
467
|
|
|
356
468
|
@socket_mode = SocketMode.new(app_token: app_token, bot_client: client)
|
|
357
|
-
@socket_mode.start do |event|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
classify_message_events([event], event.raw)
|
|
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)
|
|
364
475
|
block.call(event)
|
|
365
476
|
end
|
|
366
477
|
end
|
|
@@ -383,9 +494,13 @@ module ChatSDK
|
|
|
383
494
|
|
|
384
495
|
private
|
|
385
496
|
|
|
497
|
+
def monotonic_now
|
|
498
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
499
|
+
end
|
|
500
|
+
|
|
386
501
|
def build_client(token: nil)
|
|
387
502
|
options = {}
|
|
388
|
-
options[:token] = token
|
|
503
|
+
options[:token] = token unless token.nil?
|
|
389
504
|
options[:proxy] = @proxy if @proxy
|
|
390
505
|
options.empty? ? ::Slack::Web::Client.new : ::Slack::Web::Client.new(options)
|
|
391
506
|
end
|
|
@@ -414,23 +529,24 @@ module ChatSDK
|
|
|
414
529
|
end
|
|
415
530
|
|
|
416
531
|
def resolve_bot_user_id(event)
|
|
417
|
-
|
|
418
|
-
if
|
|
419
|
-
installation = get_installation(team_id) if team_id
|
|
420
|
-
return installation["bot_user_id"] if installation && installation["bot_user_id"]
|
|
421
|
-
end
|
|
532
|
+
request_context = context
|
|
533
|
+
return request_context[:bot_user_id] if request_context && request_context[:bot_user_id]
|
|
422
534
|
|
|
423
535
|
return @bot_user_id if @bot_user_id
|
|
424
536
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
else
|
|
428
|
-
@client
|
|
537
|
+
installation_id = if multi_workspace?
|
|
538
|
+
request_context&.[](:installation_id) || event_installation_id(event)
|
|
429
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
|
|
430
546
|
return unless current_client
|
|
431
547
|
|
|
432
548
|
@bot_user_id_mutex.synchronize do
|
|
433
|
-
cache_key =
|
|
549
|
+
cache_key = multi_workspace? ? (installation_id || :default) : :default
|
|
434
550
|
return @bot_user_ids[cache_key] if @bot_user_ids.key?(cache_key)
|
|
435
551
|
|
|
436
552
|
bot_user_id = begin
|
|
@@ -460,22 +576,142 @@ module ChatSDK
|
|
|
460
576
|
Array(element["elements"]).any? { |child| rich_text_element_mentions_bot?(child, bot_user_id) }
|
|
461
577
|
end
|
|
462
578
|
|
|
463
|
-
def
|
|
464
|
-
|
|
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
|
|
465
596
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
470
613
|
end
|
|
471
614
|
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|
628
|
+
|
|
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")
|
|
645
|
+
return
|
|
646
|
+
end
|
|
647
|
+
|
|
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
|
|
474
712
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
::Thread.current[:chat_sdk_slack_client] = new_client
|
|
478
|
-
new_client
|
|
713
|
+
def truthy_slack_value?(value)
|
|
714
|
+
value == true || value == "true"
|
|
479
715
|
end
|
|
480
716
|
|
|
481
717
|
def installation_key(team_id)
|
|
@@ -22,6 +22,13 @@ module ChatSDK
|
|
|
22
22
|
if node.attributes[:subtitle]
|
|
23
23
|
blocks << {type: "context", elements: [{type: "mrkdwn", text: truncate_mrkdwn(node.attributes[:subtitle], 3000)}]}
|
|
24
24
|
end
|
|
25
|
+
if node.attributes[:image_url] && !node.attributes[:image_url].empty?
|
|
26
|
+
blocks << {
|
|
27
|
+
type: "image",
|
|
28
|
+
image_url: node.attributes[:image_url],
|
|
29
|
+
alt_text: truncate_text(node.attributes[:title] || "Card image", 2000)
|
|
30
|
+
}
|
|
31
|
+
end
|
|
25
32
|
blocks.concat(node.children.map { |child| render_node(child) }.compact)
|
|
26
33
|
blocks
|
|
27
34
|
end
|
|
@@ -29,6 +36,7 @@ module ChatSDK
|
|
|
29
36
|
def render_node(node)
|
|
30
37
|
case node.type
|
|
31
38
|
when :text then render_text(node)
|
|
39
|
+
when :link then render_link(node)
|
|
32
40
|
when :divider then {type: "divider"}
|
|
33
41
|
when :image then render_image(node)
|
|
34
42
|
when :fields then render_fields(node)
|
|
@@ -41,12 +49,23 @@ module ChatSDK
|
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
def render_text(node)
|
|
52
|
+
content = node.attributes[:content]
|
|
53
|
+
content = "*#{truncate_mrkdwn(content, 2998)}*" if node.attributes[:style] == :bold
|
|
54
|
+
return {type: "context", elements: [{type: "mrkdwn", text: truncate_mrkdwn(content, 3000)}]} if node.attributes[:style] == :muted
|
|
55
|
+
|
|
44
56
|
{
|
|
45
57
|
type: "section",
|
|
46
|
-
text: {type: "mrkdwn", text: truncate_mrkdwn(
|
|
58
|
+
text: {type: "mrkdwn", text: truncate_mrkdwn(content, 3000)}
|
|
47
59
|
}
|
|
48
60
|
end
|
|
49
61
|
|
|
62
|
+
def render_link(node)
|
|
63
|
+
url = node.attributes[:url]
|
|
64
|
+
label_budget = [2997 - url.to_s.length, 0].max
|
|
65
|
+
label = truncate_mrkdwn(node.attributes[:label], label_budget)
|
|
66
|
+
{type: "section", text: {type: "mrkdwn", text: "<#{url}|#{label}>"}}
|
|
67
|
+
end
|
|
68
|
+
|
|
50
69
|
def render_image(node)
|
|
51
70
|
block = {type: "image", image_url: node.attributes[:url]}
|
|
52
71
|
block[:alt_text] = truncate_text(node.attributes[:alt] || " ", 2000)
|
|
@@ -64,9 +83,14 @@ module ChatSDK
|
|
|
64
83
|
|
|
65
84
|
def render_section(node)
|
|
66
85
|
block = {type: "section"}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
lines = node.children.filter_map do |child|
|
|
87
|
+
case child.type
|
|
88
|
+
when :text then child.attributes[:content]
|
|
89
|
+
when :link then render_link(child).dig(:text, :text)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
if lines.any?
|
|
93
|
+
block[:text] = {type: "mrkdwn", text: truncate_mrkdwn(lines.join("\n"), 3000)}
|
|
70
94
|
end
|
|
71
95
|
block
|
|
72
96
|
end
|
|
@@ -83,6 +107,7 @@ module ChatSDK
|
|
|
83
107
|
when :button then render_button(node)
|
|
84
108
|
when :link_button then render_link_button(node)
|
|
85
109
|
when :select then render_select(node)
|
|
110
|
+
when :radio_select then render_radio_select(node)
|
|
86
111
|
end
|
|
87
112
|
end
|
|
88
113
|
|
|
@@ -109,6 +134,11 @@ module ChatSDK
|
|
|
109
134
|
url: node.attributes[:url],
|
|
110
135
|
action_id: node.attributes[:id] || "link_#{node.attributes[:url].hash.abs}"
|
|
111
136
|
}
|
|
137
|
+
if node.attributes[:style] == :primary
|
|
138
|
+
button[:style] = "primary"
|
|
139
|
+
elsif node.attributes[:style] == :danger
|
|
140
|
+
button[:style] = "danger"
|
|
141
|
+
end
|
|
112
142
|
button[:visible_to_user_ids] = node.attributes[:visible_to] if node.attributes[:visible_to]&.any?
|
|
113
143
|
button
|
|
114
144
|
end
|
|
@@ -122,9 +152,32 @@ module ChatSDK
|
|
|
122
152
|
if node.attributes[:placeholder]
|
|
123
153
|
sel[:placeholder] = {type: "plain_text", text: truncate_text(node.attributes[:placeholder], 150)}
|
|
124
154
|
end
|
|
155
|
+
if node.attributes[:initial_option] && !node.attributes[:initial_option].empty?
|
|
156
|
+
initial_option = sel[:options].find { |option| option[:value] == node.attributes[:initial_option] }
|
|
157
|
+
sel[:initial_option] = initial_option if initial_option
|
|
158
|
+
end
|
|
125
159
|
sel
|
|
126
160
|
end
|
|
127
161
|
|
|
162
|
+
def render_radio_select(node)
|
|
163
|
+
options = node.children.first(10).map do |option|
|
|
164
|
+
rendered = {
|
|
165
|
+
text: {type: "mrkdwn", text: truncate_mrkdwn(option.attributes[:text], 75)},
|
|
166
|
+
value: option.attributes[:value]
|
|
167
|
+
}
|
|
168
|
+
if option.attributes[:description]
|
|
169
|
+
rendered[:description] = {type: "mrkdwn", text: truncate_mrkdwn(option.attributes[:description], 75)}
|
|
170
|
+
end
|
|
171
|
+
rendered
|
|
172
|
+
end
|
|
173
|
+
element = {type: "radio_buttons", action_id: node.attributes[:id], options: options}
|
|
174
|
+
if node.attributes[:initial_option] && !node.attributes[:initial_option].empty?
|
|
175
|
+
initial_option = options.find { |option| option[:value] == node.attributes[:initial_option] }
|
|
176
|
+
element[:initial_option] = initial_option if initial_option
|
|
177
|
+
end
|
|
178
|
+
element
|
|
179
|
+
end
|
|
180
|
+
|
|
128
181
|
def render_option(node)
|
|
129
182
|
opt = {
|
|
130
183
|
text: {type: "plain_text", text: truncate_text(node.attributes[:text], 75)},
|