chat_sdk-slack 1.2.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 +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
|
@@ -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"],
|
|
@@ -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
|