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
|
@@ -15,12 +15,28 @@ module ChatSDK
|
|
|
15
15
|
def render_card(node)
|
|
16
16
|
@used_native_table = false
|
|
17
17
|
@chart_count = 0
|
|
18
|
-
|
|
18
|
+
blocks = []
|
|
19
|
+
if node.attributes[:title]
|
|
20
|
+
blocks << {type: "header", text: {type: "plain_text", text: truncate_text(node.attributes[:title], 150)}}
|
|
21
|
+
end
|
|
22
|
+
if node.attributes[:subtitle]
|
|
23
|
+
blocks << {type: "context", elements: [{type: "mrkdwn", text: truncate_mrkdwn(node.attributes[:subtitle], 3000)}]}
|
|
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
|
|
32
|
+
blocks.concat(node.children.map { |child| render_node(child) }.compact)
|
|
33
|
+
blocks
|
|
19
34
|
end
|
|
20
35
|
|
|
21
36
|
def render_node(node)
|
|
22
37
|
case node.type
|
|
23
38
|
when :text then render_text(node)
|
|
39
|
+
when :link then render_link(node)
|
|
24
40
|
when :divider then {type: "divider"}
|
|
25
41
|
when :image then render_image(node)
|
|
26
42
|
when :fields then render_fields(node)
|
|
@@ -28,19 +44,31 @@ module ChatSDK
|
|
|
28
44
|
when :actions then render_actions(node)
|
|
29
45
|
when :table then render_table(node)
|
|
30
46
|
when :chart then render_chart(node)
|
|
47
|
+
when :text_input then {type: "section", text: {type: "mrkdwn", text: truncate_mrkdwn(node.attributes[:label], 3000)}}
|
|
31
48
|
end
|
|
32
49
|
end
|
|
33
50
|
|
|
34
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
|
+
|
|
35
56
|
{
|
|
36
57
|
type: "section",
|
|
37
|
-
text: {type: "mrkdwn", text:
|
|
58
|
+
text: {type: "mrkdwn", text: truncate_mrkdwn(content, 3000)}
|
|
38
59
|
}
|
|
39
60
|
end
|
|
40
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
|
+
|
|
41
69
|
def render_image(node)
|
|
42
70
|
block = {type: "image", image_url: node.attributes[:url]}
|
|
43
|
-
block[:alt_text] = node.attributes[:alt] || " "
|
|
71
|
+
block[:alt_text] = truncate_text(node.attributes[:alt] || " ", 2000)
|
|
44
72
|
block
|
|
45
73
|
end
|
|
46
74
|
|
|
@@ -48,16 +76,21 @@ module ChatSDK
|
|
|
48
76
|
{
|
|
49
77
|
type: "section",
|
|
50
78
|
fields: node.children.map do |field|
|
|
51
|
-
{type: "mrkdwn", text: "*#{field.attributes[:label]}*\n#{field.attributes[:value]}"}
|
|
79
|
+
{type: "mrkdwn", text: truncate_mrkdwn("*#{field.attributes[:label]}*\n#{field.attributes[:value]}", 2000)}
|
|
52
80
|
end
|
|
53
81
|
}
|
|
54
82
|
end
|
|
55
83
|
|
|
56
84
|
def render_section(node)
|
|
57
85
|
block = {type: "section"}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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)}
|
|
61
94
|
end
|
|
62
95
|
block
|
|
63
96
|
end
|
|
@@ -74,13 +107,14 @@ module ChatSDK
|
|
|
74
107
|
when :button then render_button(node)
|
|
75
108
|
when :link_button then render_link_button(node)
|
|
76
109
|
when :select then render_select(node)
|
|
110
|
+
when :radio_select then render_radio_select(node)
|
|
77
111
|
end
|
|
78
112
|
end
|
|
79
113
|
|
|
80
114
|
def render_button(node)
|
|
81
115
|
btn = {
|
|
82
116
|
type: "button",
|
|
83
|
-
text: {type: "plain_text", text: node.attributes[:text]},
|
|
117
|
+
text: {type: "plain_text", text: truncate_text(node.attributes[:text], 75)},
|
|
84
118
|
action_id: node.attributes[:id]
|
|
85
119
|
}
|
|
86
120
|
btn[:value] = node.attributes[:value] if node.attributes[:value]
|
|
@@ -89,16 +123,24 @@ module ChatSDK
|
|
|
89
123
|
elsif node.attributes[:style] == :danger
|
|
90
124
|
btn[:style] = "danger"
|
|
91
125
|
end
|
|
126
|
+
btn[:visible_to_user_ids] = node.attributes[:visible_to] if node.attributes[:visible_to]&.any?
|
|
92
127
|
btn
|
|
93
128
|
end
|
|
94
129
|
|
|
95
130
|
def render_link_button(node)
|
|
96
|
-
{
|
|
131
|
+
button = {
|
|
97
132
|
type: "button",
|
|
98
|
-
text: {type: "plain_text", text: node.attributes[:text]},
|
|
133
|
+
text: {type: "plain_text", text: truncate_text(node.attributes[:text], 75)},
|
|
99
134
|
url: node.attributes[:url],
|
|
100
135
|
action_id: node.attributes[:id] || "link_#{node.attributes[:url].hash.abs}"
|
|
101
136
|
}
|
|
137
|
+
if node.attributes[:style] == :primary
|
|
138
|
+
button[:style] = "primary"
|
|
139
|
+
elsif node.attributes[:style] == :danger
|
|
140
|
+
button[:style] = "danger"
|
|
141
|
+
end
|
|
142
|
+
button[:visible_to_user_ids] = node.attributes[:visible_to] if node.attributes[:visible_to]&.any?
|
|
143
|
+
button
|
|
102
144
|
end
|
|
103
145
|
|
|
104
146
|
def render_select(node)
|
|
@@ -108,18 +150,41 @@ module ChatSDK
|
|
|
108
150
|
options: node.children.map { |opt| render_option(opt) }
|
|
109
151
|
}
|
|
110
152
|
if node.attributes[:placeholder]
|
|
111
|
-
sel[:placeholder] = {type: "plain_text", text: node.attributes[:placeholder]}
|
|
153
|
+
sel[:placeholder] = {type: "plain_text", text: truncate_text(node.attributes[:placeholder], 150)}
|
|
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
|
|
112
158
|
end
|
|
113
159
|
sel
|
|
114
160
|
end
|
|
115
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
|
+
|
|
116
181
|
def render_option(node)
|
|
117
182
|
opt = {
|
|
118
|
-
text: {type: "plain_text", text: node.attributes[:text]},
|
|
183
|
+
text: {type: "plain_text", text: truncate_text(node.attributes[:text], 75)},
|
|
119
184
|
value: node.attributes[:value]
|
|
120
185
|
}
|
|
121
186
|
if node.attributes[:description]
|
|
122
|
-
opt[:description] = {type: "plain_text", text: node.attributes[:description]}
|
|
187
|
+
opt[:description] = {type: "plain_text", text: truncate_text(node.attributes[:description], 75)}
|
|
123
188
|
end
|
|
124
189
|
opt
|
|
125
190
|
end
|
|
@@ -190,7 +255,46 @@ module ChatSDK
|
|
|
190
255
|
end
|
|
191
256
|
|
|
192
257
|
def fallback_block(text)
|
|
193
|
-
{type: "section", text: {type: "mrkdwn", text: "```#{text}```"}}
|
|
258
|
+
{type: "section", text: {type: "mrkdwn", text: truncate_mrkdwn("```#{text}```", 3000)}}
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def truncate_text(text, limit)
|
|
262
|
+
text = text.to_s
|
|
263
|
+
return text if text.length <= limit
|
|
264
|
+
|
|
265
|
+
"#{text[0, limit - 1]}…"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def truncate_mrkdwn(text, limit)
|
|
269
|
+
text = text.to_s
|
|
270
|
+
return text if text.length <= limit
|
|
271
|
+
|
|
272
|
+
prefix = clean_prefix(text[0, limit - 1])
|
|
273
|
+
4.times do
|
|
274
|
+
bold_close = prefix.count("*").odd? ? "*" : ""
|
|
275
|
+
fence_close = open_fence?(prefix) ? "\n```" : ""
|
|
276
|
+
available = limit - 1 - bold_close.length - fence_close.length
|
|
277
|
+
next_prefix = clean_prefix(text[0, available])
|
|
278
|
+
break if next_prefix == prefix
|
|
279
|
+
|
|
280
|
+
prefix = next_prefix
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
bold_close = prefix.count("*").odd? ? "*" : ""
|
|
284
|
+
fence_close = open_fence?(prefix) ? "\n```" : ""
|
|
285
|
+
suffix = "#{bold_close}…#{fence_close}"
|
|
286
|
+
prefix = clean_prefix(prefix[0, limit - suffix.length]) while prefix.length + suffix.length > limit
|
|
287
|
+
"#{prefix}#{suffix}"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def clean_prefix(text)
|
|
291
|
+
text
|
|
292
|
+
.sub(/<(?:[@#!]|[a-z][a-z\d+.-]*(?::|$))[^>]*\z/i, "")
|
|
293
|
+
.sub(/&(?:#?[a-z\d]*)?\z/i, "")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def open_fence?(text)
|
|
297
|
+
text.scan("```").length.odd?
|
|
194
298
|
end
|
|
195
299
|
end
|
|
196
300
|
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module ChatSDK
|
|
6
|
+
module Slack
|
|
7
|
+
class ChatStreamer
|
|
8
|
+
attr_reader :ts
|
|
9
|
+
|
|
10
|
+
def initialize(client, channel:, thread_ts:, recipient_user_id: nil,
|
|
11
|
+
recipient_team_id: nil, task_display_mode: nil, buffer_size: 256)
|
|
12
|
+
@client = client
|
|
13
|
+
@stream_args = {channel: channel, thread_ts: thread_ts}
|
|
14
|
+
@stream_args[:recipient_user_id] = recipient_user_id if recipient_user_id
|
|
15
|
+
@stream_args[:recipient_team_id] = recipient_team_id if recipient_team_id
|
|
16
|
+
@stream_args[:task_display_mode] = task_display_mode if task_display_mode
|
|
17
|
+
@buffer_size = buffer_size
|
|
18
|
+
@buffer = +""
|
|
19
|
+
@state = :starting
|
|
20
|
+
@token = nil
|
|
21
|
+
@ts = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def append(markdown_text: nil, chunks: nil, **options)
|
|
25
|
+
ensure_open!(:append)
|
|
26
|
+
@token = options.delete(:token) if options[:token]
|
|
27
|
+
@buffer << markdown_text if markdown_text && !markdown_text.empty?
|
|
28
|
+
return unless @buffer.length >= @buffer_size || !chunks.nil?
|
|
29
|
+
|
|
30
|
+
flush_buffer(chunks, options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stop(markdown_text: nil, chunks: nil, blocks: nil, **options)
|
|
34
|
+
ensure_open!(:stop)
|
|
35
|
+
@token = options.delete(:token) if options[:token]
|
|
36
|
+
@buffer << markdown_text if markdown_text && !markdown_text.empty?
|
|
37
|
+
|
|
38
|
+
start_stream unless ts
|
|
39
|
+
|
|
40
|
+
stop_chunks = chunks_for_flush(chunks)
|
|
41
|
+
params = {
|
|
42
|
+
token: @token,
|
|
43
|
+
channel: @stream_args[:channel],
|
|
44
|
+
ts: ts,
|
|
45
|
+
chunks: JSON.generate(stop_chunks)
|
|
46
|
+
}
|
|
47
|
+
params[:blocks] = JSON.generate(blocks) if blocks
|
|
48
|
+
params.merge!(options)
|
|
49
|
+
response = call_api(:chat_stopStream, "chat.stopStream", params)
|
|
50
|
+
@buffer.clear
|
|
51
|
+
@state = :completed
|
|
52
|
+
response
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def flush_buffer(chunks, options)
|
|
58
|
+
payload = chunks_for_flush(chunks)
|
|
59
|
+
params = {
|
|
60
|
+
token: @token,
|
|
61
|
+
chunks: JSON.generate(payload)
|
|
62
|
+
}.merge(options)
|
|
63
|
+
|
|
64
|
+
if ts
|
|
65
|
+
params[:channel] = @stream_args[:channel]
|
|
66
|
+
params[:ts] = ts
|
|
67
|
+
response = call_api(:chat_appendStream, "chat.appendStream", params)
|
|
68
|
+
else
|
|
69
|
+
response = call_api(
|
|
70
|
+
:chat_startStream,
|
|
71
|
+
"chat.startStream",
|
|
72
|
+
@stream_args.merge(params)
|
|
73
|
+
)
|
|
74
|
+
@ts = response_value(response, :ts)
|
|
75
|
+
@state = :in_progress
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@buffer.clear
|
|
79
|
+
response
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def start_stream
|
|
83
|
+
response = call_api(
|
|
84
|
+
:chat_startStream,
|
|
85
|
+
"chat.startStream",
|
|
86
|
+
@stream_args.merge(token: @token)
|
|
87
|
+
)
|
|
88
|
+
@ts = response_value(response, :ts)
|
|
89
|
+
raise ChatSDK::Error, "failed to stop stream: stream not started" unless ts
|
|
90
|
+
|
|
91
|
+
@state = :in_progress
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def chunks_for_flush(chunks)
|
|
95
|
+
payload = []
|
|
96
|
+
unless @buffer.empty?
|
|
97
|
+
payload << {type: "markdown_text", text: @buffer}
|
|
98
|
+
end
|
|
99
|
+
Array(chunks).each do |chunk|
|
|
100
|
+
payload << (chunk.respond_to?(:to_h) ? chunk.to_h : chunk)
|
|
101
|
+
end
|
|
102
|
+
payload
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def call_api(method, endpoint, params)
|
|
106
|
+
if @client.respond_to?(method)
|
|
107
|
+
@client.public_send(method, **params)
|
|
108
|
+
else
|
|
109
|
+
@client.post(endpoint, params)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def response_value(response, key)
|
|
114
|
+
response[key.to_s] || response[key]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def ensure_open!(action)
|
|
118
|
+
return unless @state == :completed
|
|
119
|
+
|
|
120
|
+
raise ChatSDK::Error, "failed to #{action} stream: stream state is #{@state}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -37,11 +37,64 @@ module ChatSDK
|
|
|
37
37
|
parse_reaction(event, payload, added: true)
|
|
38
38
|
when "reaction_removed"
|
|
39
39
|
parse_reaction(event, payload, added: false)
|
|
40
|
+
when "assistant_thread_started"
|
|
41
|
+
parse_assistant_thread(event, payload, ChatSDK::Events::AssistantThreadStarted)
|
|
42
|
+
when "assistant_thread_context_changed"
|
|
43
|
+
parse_assistant_thread(event, payload, ChatSDK::Events::AssistantContextChanged)
|
|
44
|
+
when "app_home_opened"
|
|
45
|
+
parse_app_home_opened(event, payload)
|
|
46
|
+
when "member_joined_channel"
|
|
47
|
+
parse_member_joined_channel(event, payload)
|
|
40
48
|
else
|
|
41
49
|
[]
|
|
42
50
|
end
|
|
43
51
|
end
|
|
44
52
|
|
|
53
|
+
def parse_assistant_thread(event, payload, event_class)
|
|
54
|
+
assistant_thread = event["assistant_thread"]
|
|
55
|
+
return [] unless assistant_thread
|
|
56
|
+
|
|
57
|
+
thread_ts = assistant_thread["thread_ts"]
|
|
58
|
+
context = assistant_thread["context"] || {}
|
|
59
|
+
[event_class.new(
|
|
60
|
+
thread_id: thread_ts,
|
|
61
|
+
channel_id: assistant_thread["channel_id"],
|
|
62
|
+
thread_ts: thread_ts,
|
|
63
|
+
user_id: assistant_thread["user_id"],
|
|
64
|
+
context: context,
|
|
65
|
+
platform: :slack,
|
|
66
|
+
adapter_name: :slack,
|
|
67
|
+
raw: payload,
|
|
68
|
+
timestamp: slack_time(event["event_ts"])
|
|
69
|
+
)]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def parse_app_home_opened(event, payload)
|
|
73
|
+
return [] unless event["tab"] == "home"
|
|
74
|
+
|
|
75
|
+
[ChatSDK::Events::AppHomeOpened.new(
|
|
76
|
+
user_id: event["user"],
|
|
77
|
+
channel_id: event["channel"],
|
|
78
|
+
tab: event["tab"],
|
|
79
|
+
platform: :slack,
|
|
80
|
+
adapter_name: :slack,
|
|
81
|
+
raw: payload,
|
|
82
|
+
timestamp: slack_time(event["event_ts"])
|
|
83
|
+
)]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def parse_member_joined_channel(event, payload)
|
|
87
|
+
[ChatSDK::Events::MemberJoinedChannel.new(
|
|
88
|
+
user_id: event["user"],
|
|
89
|
+
channel_id: event["channel"],
|
|
90
|
+
inviter_id: event["inviter"],
|
|
91
|
+
platform: :slack,
|
|
92
|
+
adapter_name: :slack,
|
|
93
|
+
raw: payload,
|
|
94
|
+
timestamp: slack_time(event["event_ts"])
|
|
95
|
+
)]
|
|
96
|
+
end
|
|
97
|
+
|
|
45
98
|
def parse_mention(event, payload)
|
|
46
99
|
author = ChatSDK::Author.new(
|
|
47
100
|
id: event["user"],
|
|
@@ -50,7 +103,7 @@ module ChatSDK
|
|
|
50
103
|
)
|
|
51
104
|
message = ChatSDK::Message.new(
|
|
52
105
|
id: event["ts"],
|
|
53
|
-
text: event
|
|
106
|
+
text: message_text(event),
|
|
54
107
|
author: author,
|
|
55
108
|
thread_id: event["thread_ts"] || event["ts"],
|
|
56
109
|
channel_id: event["channel"],
|
|
@@ -80,7 +133,7 @@ module ChatSDK
|
|
|
80
133
|
)
|
|
81
134
|
message = ChatSDK::Message.new(
|
|
82
135
|
id: event["ts"],
|
|
83
|
-
text: event
|
|
136
|
+
text: message_text(event),
|
|
84
137
|
author: author,
|
|
85
138
|
thread_id: event["thread_ts"] || event["ts"],
|
|
86
139
|
channel_id: event["channel"],
|
|
@@ -130,6 +183,37 @@ module ChatSDK
|
|
|
130
183
|
)]
|
|
131
184
|
end
|
|
132
185
|
|
|
186
|
+
def message_text(event)
|
|
187
|
+
blocks = event["blocks"]
|
|
188
|
+
text = if blocks.is_a?(Array) && !blocks.empty? && blocks.all? { |block| %w[section header context image divider actions table].include?(block["type"]) }
|
|
189
|
+
blocks.filter_map { |block| block_text(block) }.join("\n")
|
|
190
|
+
else
|
|
191
|
+
event["text"] || ""
|
|
192
|
+
end
|
|
193
|
+
FormatConverter.new.normalize_inbound_text(text)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def block_text(block)
|
|
197
|
+
case block["type"]
|
|
198
|
+
when "section", "header"
|
|
199
|
+
texts = [block.dig("text", "text")]
|
|
200
|
+
texts.concat(Array(block["fields"]).map { |field| field["text"] })
|
|
201
|
+
texts.compact.reject(&:empty?).join("\n")
|
|
202
|
+
when "context"
|
|
203
|
+
Array(block["elements"]).filter_map { |element| element.dig("text") }.reject(&:empty?).join("\n")
|
|
204
|
+
when "image"
|
|
205
|
+
block["alt_text"]
|
|
206
|
+
when "actions"
|
|
207
|
+
Array(block["elements"]).filter_map do |element|
|
|
208
|
+
element.dig("text", "text") || element.dig("placeholder", "text")
|
|
209
|
+
end.reject(&:empty?).join(" ")
|
|
210
|
+
when "table"
|
|
211
|
+
Array(block["rows"]).map do |row|
|
|
212
|
+
Array(row).map { |cell| cell.dig("text") || cell.to_s }.join(" | ")
|
|
213
|
+
end.join("\n")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
133
217
|
def parse_message_deleted(event, payload)
|
|
134
218
|
previous = event["previous_message"]
|
|
135
219
|
message_id = event["deleted_ts"] || previous&.dig("ts")
|
|
@@ -19,6 +19,23 @@ module ChatSDK
|
|
|
19
19
|
parts.map.with_index { |part, i| i.odd? ? part : convert_md_to_mrkdwn(part) }.join
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def normalize_inbound_text(text)
|
|
23
|
+
return "" if text.nil? || text.empty?
|
|
24
|
+
|
|
25
|
+
split_code_blocks(text).map.with_index do |part, index|
|
|
26
|
+
if index.odd?
|
|
27
|
+
part
|
|
28
|
+
else
|
|
29
|
+
part
|
|
30
|
+
.gsub("<!here>", "@here")
|
|
31
|
+
.gsub("<!channel>", "@channel")
|
|
32
|
+
.gsub("<!everyone>", "@everyone")
|
|
33
|
+
.gsub(/<!subteam\^[^|>]+\|(@[^>]+)>/, '\1')
|
|
34
|
+
.gsub(/<(?:mailto|tel):[^>|]+\|([^>]+)>/, '\1')
|
|
35
|
+
end
|
|
36
|
+
end.join
|
|
37
|
+
end
|
|
38
|
+
|
|
22
39
|
private
|
|
23
40
|
|
|
24
41
|
# ── Slack mrkdwn → Markdown ──────────────────────────────────────────
|
|
@@ -11,6 +11,9 @@ module ChatSDK
|
|
|
11
11
|
title: {type: "plain_text", text: node.attributes[:title]}
|
|
12
12
|
}
|
|
13
13
|
view[:callback_id] = node.attributes[:callback_id] if node.attributes[:callback_id]
|
|
14
|
+
if node.attributes[:close_label] && !node.attributes[:close_label].empty?
|
|
15
|
+
view[:close] = {type: "plain_text", text: node.attributes[:close_label]}
|
|
16
|
+
end
|
|
14
17
|
if node.attributes[:submit_label]
|
|
15
18
|
view[:submit] = {type: "plain_text", text: node.attributes[:submit_label]}
|
|
16
19
|
end
|
|
@@ -24,6 +27,7 @@ module ChatSDK
|
|
|
24
27
|
case node.type
|
|
25
28
|
when :input then render_input(node)
|
|
26
29
|
when :text then render_text(node)
|
|
30
|
+
when :fields then render_fields(node)
|
|
27
31
|
end
|
|
28
32
|
end
|
|
29
33
|
|
|
@@ -42,6 +46,10 @@ module ChatSDK
|
|
|
42
46
|
multiline: !!node.attributes[:multiline]
|
|
43
47
|
}
|
|
44
48
|
el[:placeholder] = {type: "plain_text", text: node.attributes[:placeholder]} if node.attributes[:placeholder]
|
|
49
|
+
if node.attributes[:initial_value] && !node.attributes[:initial_value].empty?
|
|
50
|
+
el[:initial_value] = node.attributes[:initial_value]
|
|
51
|
+
end
|
|
52
|
+
el[:max_length] = node.attributes[:max_length] if node.attributes[:max_length] && !node.attributes[:max_length].zero?
|
|
45
53
|
el
|
|
46
54
|
when :select
|
|
47
55
|
el = {
|
|
@@ -50,6 +58,26 @@ module ChatSDK
|
|
|
50
58
|
options: node.children.map { |opt| render_option(opt) }
|
|
51
59
|
}
|
|
52
60
|
el[:placeholder] = {type: "plain_text", text: node.attributes[:placeholder]} if node.attributes[:placeholder]
|
|
61
|
+
if node.attributes[:initial_option] && !node.attributes[:initial_option].empty?
|
|
62
|
+
initial_option = el[:options].find { |option| option[:value] == node.attributes[:initial_option] }
|
|
63
|
+
el[:initial_option] = initial_option if initial_option
|
|
64
|
+
end
|
|
65
|
+
el
|
|
66
|
+
when :radio_select
|
|
67
|
+
options = node.children.first(10).map { |option| render_radio_option(option) }
|
|
68
|
+
el = {type: "radio_buttons", action_id: node.attributes[:id], options: options}
|
|
69
|
+
if node.attributes[:initial_option] && !node.attributes[:initial_option].empty?
|
|
70
|
+
initial_option = options.find { |option| option[:value] == node.attributes[:initial_option] }
|
|
71
|
+
el[:initial_option] = initial_option if initial_option
|
|
72
|
+
end
|
|
73
|
+
el
|
|
74
|
+
when :external_select
|
|
75
|
+
el = {type: "external_select", action_id: node.attributes[:id]}
|
|
76
|
+
el[:placeholder] = {type: "plain_text", text: node.attributes[:placeholder]} if node.attributes[:placeholder]
|
|
77
|
+
el[:min_query_length] = node.attributes[:min_query_length] unless node.attributes[:min_query_length].nil?
|
|
78
|
+
if node.attributes[:initial_option]
|
|
79
|
+
el[:initial_option] = render_external_option(node.attributes[:initial_option])
|
|
80
|
+
end
|
|
53
81
|
el
|
|
54
82
|
when :date
|
|
55
83
|
el = {type: "datepicker", action_id: node.attributes[:id]}
|
|
@@ -69,6 +97,7 @@ module ChatSDK
|
|
|
69
97
|
el[:placeholder] = {type: "plain_text", text: node.attributes[:placeholder]} if node.attributes[:placeholder]
|
|
70
98
|
el
|
|
71
99
|
end
|
|
100
|
+
block[:dispatch_action] = node.attributes[:dispatch_action] unless node.attributes[:dispatch_action].nil?
|
|
72
101
|
block
|
|
73
102
|
end
|
|
74
103
|
|
|
@@ -97,6 +126,33 @@ module ChatSDK
|
|
|
97
126
|
opt[:description] = {type: "plain_text", text: node.attributes[:description]} if node.attributes[:description]
|
|
98
127
|
opt
|
|
99
128
|
end
|
|
129
|
+
|
|
130
|
+
def render_radio_option(node)
|
|
131
|
+
option = {
|
|
132
|
+
text: {type: "mrkdwn", text: node.attributes[:text]},
|
|
133
|
+
value: node.attributes[:value]
|
|
134
|
+
}
|
|
135
|
+
option[:description] = {type: "mrkdwn", text: node.attributes[:description]} if node.attributes[:description]
|
|
136
|
+
option
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def render_external_option(option)
|
|
140
|
+
label = option[:label] || option["label"]
|
|
141
|
+
value = option[:value] || option["value"]
|
|
142
|
+
description = option[:description] || option["description"]
|
|
143
|
+
rendered = {text: {type: "plain_text", text: label}, value: value}
|
|
144
|
+
rendered[:description] = {type: "plain_text", text: description} if description
|
|
145
|
+
rendered
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def render_fields(node)
|
|
149
|
+
{
|
|
150
|
+
type: "section",
|
|
151
|
+
fields: node.children.map do |field|
|
|
152
|
+
{type: "mrkdwn", text: "*#{field.attributes[:label]}*\n#{field.attributes[:value]}"}
|
|
153
|
+
end
|
|
154
|
+
}
|
|
155
|
+
end
|
|
100
156
|
end
|
|
101
157
|
end
|
|
102
158
|
end
|