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.
@@ -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