chat_sdk-teams 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a6410c72972c0210380902fcf4a176c992c68d7c18154ff55f2a286fe1823a2
4
- data.tar.gz: 59897106f6792d7ec3c73b76de36019fa1eb40126ec120bf28060fe35cc47cc2
3
+ metadata.gz: f16daba221cded2918a20444a752ac5c47d62579091e385fc09a916f9886cee9
4
+ data.tar.gz: 84470e0eaf8edc9eaeafd70d5e541ed0553c1ede14a1ee627f735d9f31aa8081
5
5
  SHA512:
6
- metadata.gz: 485f8fea6b1f2702b71a7b1a78cbef630255c0a88d92d5b8e04a723bc30fd7629746eb9a84fc6e99871baec9dc488aeb7ee2bfebfaacf60a95e298f48a3b4571
7
- data.tar.gz: f25e032254fee24a840cd7c14afd317ea45c279e404f2bfb6cf5a494d20803f980f2452d7e6a99e6f3d7b485a435c6cf4316682b3cdc3737fcd45eb5072ef8ab
6
+ metadata.gz: ea22ecba125cdbc9865e043d570ea2641cd4df7bd6cb841fed57e2fe999a9027c1194b874d07e6757ca9033268ec82466ec982ea41e1362695b356dd0f33f9e9
7
+ data.tar.gz: 140ab32b11655a4732c06d15d0911600d6730d92f8f49963340b7e7a056b9e3050e893cd67e923bd32dccf4ab7b06e7d1797538c267322fea093c379e2a55702
@@ -133,6 +133,10 @@ module ChatSDK
133
133
  action_id ||= action["verb"] if action.is_a?(Hash)
134
134
  action_id ||= data["actionId"] || data["action"] ||
135
135
  (action unless action.is_a?(Hash)) || activity["name"] || "invoke"
136
+ if action_id == AUTO_SUBMIT_ACTION_ID
137
+ return auto_submit_actions(data, activity, conversation_id, user)
138
+ end
139
+
136
140
  value = data["value"] || (nested_data unless nested_data.is_a?(Hash)) || data
137
141
  value = value.is_a?(Hash) ? JSON.generate(value) : value.to_s
138
142
 
@@ -223,6 +227,41 @@ module ChatSDK
223
227
  values.empty? ? nil : values
224
228
  end
225
229
 
230
+ def auto_submit_actions(data, activity, conversation_id, user)
231
+ values = auto_submit_values(data)
232
+ values = nil if values.empty?
233
+
234
+ data.each_with_object([]) do |(action_id, input_value), events|
235
+ next if %w[actionId msteams].include?(action_id)
236
+
237
+ events << ChatSDK::Events::Action.new(
238
+ action_id: action_id,
239
+ value: submit_value_to_string(input_value),
240
+ values: values,
241
+ user: user,
242
+ thread_id: conversation_id,
243
+ channel_id: conversation_id,
244
+ platform: :teams,
245
+ adapter_name: :teams,
246
+ raw: activity
247
+ )
248
+ end
249
+ end
250
+
251
+ def auto_submit_values(data)
252
+ data.each_with_object({}) do |(key, value), inputs|
253
+ next if %w[actionId msteams].include?(key)
254
+
255
+ input_value = submit_value_to_string(value)
256
+ inputs[key] = input_value unless input_value.nil?
257
+ end
258
+ end
259
+
260
+ def submit_value_to_string(value)
261
+ return value if value.is_a?(String)
262
+ value.to_s if value.is_a?(Numeric)
263
+ end
264
+
226
265
  def bot_mentioned?(activity, bot_app_id)
227
266
  return false unless bot_app_id
228
267
 
@@ -6,6 +6,7 @@ module ChatSDK
6
6
  module Teams
7
7
  class Adapter < ChatSDK::Adapter::Base
8
8
  DEFAULT_SERVICE_URL = "https://smba.trafficmanager.net/teams/"
9
+ USER_SERVICE_URL_TTL = 30 * 24 * 60 * 60
9
10
  VERIFIED_JWT_CLAIMS_ENV_KEY = "chat_sdk.teams.verified_jwt_claims"
10
11
 
11
12
  capabilities :edit_messages, :delete_messages, :threads, :direct_messages,
@@ -58,8 +59,10 @@ module ChatSDK
58
59
 
59
60
  conversation_id = activity.dig("conversation", "id")
60
61
  claims = rack_request.env[VERIFIED_JWT_CLAIMS_ENV_KEY]
61
- if conversation_id && trusted_service_url?(activity["serviceUrl"], claims)
62
- register_service_url(conversation_id, activity["serviceUrl"])
62
+ if trusted_service_url?(activity["serviceUrl"], claims)
63
+ register_service_url(conversation_id, activity["serviceUrl"]) if conversation_id
64
+ user_id = activity.dig("from", "id")
65
+ @state&.set(user_service_url_key(user_id), activity["serviceUrl"], ttl: USER_SERVICE_URL_TTL) if user_id
63
66
  end
64
67
 
65
68
  ActivityParser.parse(activity, bot_app_id: @app_id)
@@ -149,7 +152,7 @@ module ChatSDK
149
152
 
150
153
  def open_dm(user_id)
151
154
  require_capability!(:direct_messages)
152
- service_url = @service_urls.values.first || @default_service_url
155
+ service_url = @state&.get(user_service_url_key(user_id)) || @service_urls.values.first || @default_service_url
153
156
 
154
157
  payload = {
155
158
  "bot" => {"id" => @app_id},
@@ -208,8 +211,10 @@ module ChatSDK
208
211
  @state&.set(service_url_key(conversation_id), service_url)
209
212
  end
210
213
 
211
- def webhook_response(events)
212
- return unless events.any? { |event| event.raw.is_a?(Hash) && event.raw["type"] == "invoke" }
214
+ def webhook_response(events, rack_request = nil)
215
+ invoke_activity = rack_request&.respond_to?(:body) && invoke_activity?(rack_request)
216
+ invoke_event = events.any? { |event| event.raw.is_a?(Hash) && event.raw["type"] == "invoke" }
217
+ return unless invoke_activity || invoke_event
213
218
 
214
219
  body = {
215
220
  "statusCode" => 200,
@@ -221,6 +226,13 @@ module ChatSDK
221
226
 
222
227
  private
223
228
 
229
+ def invoke_activity?(rack_request)
230
+ activity = read_json_body(rack_request)
231
+ activity.is_a?(Hash) && activity["type"] == "invoke"
232
+ rescue JSON::ParserError
233
+ false
234
+ end
235
+
224
236
  def build_activity(postable_message)
225
237
  activity = {"type" => "message"}
226
238
 
@@ -248,12 +260,7 @@ module ChatSDK
248
260
  end
249
261
 
250
262
  def service_url_for(channel_id)
251
- url = @service_urls[channel_id]
252
- if !url && @state
253
- url = @state.get(service_url_key(channel_id))
254
- @service_urls[channel_id] = url if url
255
- end
256
- url ||= @default_service_url
263
+ url = @service_urls[channel_id] || @state&.get(service_url_key(channel_id)) || @default_service_url
257
264
  unless url
258
265
  raise ChatSDK::PlatformError.new(
259
266
  "No service URL for conversation #{channel_id}. Register it first or receive an activity.",
@@ -267,6 +274,10 @@ module ChatSDK
267
274
  "chat_sdk:teams:service_url:#{conversation_id}"
268
275
  end
269
276
 
277
+ def user_service_url_key(user_id)
278
+ "chat_sdk:teams:user_service_url:#{user_id}"
279
+ end
280
+
270
281
  def trusted_service_url?(service_url, claims)
271
282
  return false unless service_url.is_a?(String)
272
283
 
@@ -6,7 +6,10 @@ module ChatSDK
6
6
  def render(node)
7
7
  case node.type
8
8
  when :card then render_card(node)
9
- else wrap_card([render_node(node)].compact)
9
+ else
10
+ rendered = render_node(node)
11
+ elements = rendered.is_a?(Array) ? rendered.flatten.compact : [rendered].compact
12
+ wrap_card(elements)
10
13
  end
11
14
  end
12
15
 
@@ -15,6 +18,13 @@ module ChatSDK
15
18
  def render_card(node)
16
19
  body = []
17
20
  pending_separator = false
21
+ if node.attributes[:image_url] && !node.attributes[:image_url].empty?
22
+ body << {
23
+ "type" => "Image",
24
+ "url" => node.attributes[:image_url],
25
+ "size" => "Stretch"
26
+ }
27
+ end
18
28
 
19
29
  node.children.each do |child|
20
30
  if child.type == :divider
@@ -25,16 +35,15 @@ module ChatSDK
25
35
  element = render_node(child)
26
36
  next unless element
27
37
 
38
+ elements = element.is_a?(Array) ? element.compact : [element]
39
+ next if elements.empty?
40
+
28
41
  if pending_separator
29
- element["separator"] = true
42
+ elements.first["separator"] = true
30
43
  pending_separator = false
31
44
  end
32
45
 
33
- if element.is_a?(Array)
34
- body.concat(element)
35
- else
36
- body << element
37
- end
46
+ body.concat(elements)
38
47
  end
39
48
 
40
49
  wrap_card(body, width: node.attributes[:width])
@@ -54,6 +63,7 @@ module ChatSDK
54
63
  def render_node(node)
55
64
  case node.type
56
65
  when :text then render_text(node)
66
+ when :link then render_link(node)
57
67
  when :divider then nil # handled in render_card
58
68
  when :image then render_image(node)
59
69
  when :fields then render_fields(node)
@@ -62,6 +72,7 @@ module ChatSDK
62
72
  when :button then render_action_submit(node)
63
73
  when :link_button then render_action_open_url(node)
64
74
  when :select then render_select(node)
75
+ when :radio_select then render_radio_select(node)
65
76
  when :text_input then render_text_input(node)
66
77
  when :table then render_table(node)
67
78
  when :chart then render_chart(node)
@@ -69,11 +80,22 @@ module ChatSDK
69
80
  end
70
81
 
71
82
  def render_text(node)
72
- {
83
+ text = {
73
84
  "type" => "TextBlock",
74
85
  "text" => node.attributes[:content],
75
86
  "wrap" => true
76
87
  }
88
+ text["weight"] = "Bolder" if node.attributes[:style] == :bold
89
+ text["isSubtle"] = true if node.attributes[:style] == :muted
90
+ text
91
+ end
92
+
93
+ def render_link(node)
94
+ {
95
+ "type" => "TextBlock",
96
+ "text" => "[#{node.attributes[:label]}](#{node.attributes[:url]})",
97
+ "wrap" => true
98
+ }
77
99
  end
78
100
 
79
101
  def render_image(node)
@@ -124,19 +146,37 @@ module ChatSDK
124
146
  end
125
147
 
126
148
  def render_actions(node)
127
- actions = node.children.filter_map { |child| render_action_element(child) }
128
- {
129
- "type" => "ActionSet",
130
- "actions" => actions
131
- }
132
- end
149
+ elements = []
150
+ actions = []
151
+ has_inputs = false
152
+ has_buttons = false
133
153
 
134
- def render_action_element(node)
135
- case node.type
136
- when :button then render_action_submit(node)
137
- when :link_button then render_action_open_url(node)
138
- when :select then render_select(node)
154
+ node.children.each do |child|
155
+ case child.type
156
+ when :button
157
+ has_buttons = true
158
+ actions << render_action_submit(child)
159
+ when :link_button
160
+ actions << render_action_open_url(child)
161
+ when :select
162
+ has_inputs = true
163
+ elements << render_select(child)
164
+ when :radio_select
165
+ has_inputs = true
166
+ elements << render_radio_select(child)
167
+ end
139
168
  end
169
+
170
+ if has_inputs && !has_buttons
171
+ actions << {
172
+ "type" => "Action.Submit",
173
+ "title" => "Submit",
174
+ "data" => {"actionId" => AUTO_SUBMIT_ACTION_ID}
175
+ }
176
+ end
177
+
178
+ elements << {"type" => "ActionSet", "actions" => actions} unless actions.empty?
179
+ elements
140
180
  end
141
181
 
142
182
  def render_action_submit(node)
@@ -163,6 +203,11 @@ module ChatSDK
163
203
  }
164
204
  action["id"] = node.attributes[:id] if node.attributes[:id]
165
205
  action["tooltip"] = node.attributes[:tooltip] if node.attributes[:tooltip]
206
+ if node.attributes[:style] == :primary
207
+ action["style"] = "positive"
208
+ elsif node.attributes[:style] == :danger
209
+ action["style"] = "destructive"
210
+ end
166
211
  action
167
212
  end
168
213
 
@@ -178,6 +223,30 @@ module ChatSDK
178
223
  end
179
224
  }
180
225
  input["placeholder"] = node.attributes[:placeholder] if node.attributes[:placeholder]
226
+ if [node.attributes[:label], node.attributes[:initial_option], node.attributes[:optional]].any? { |value| !value.nil? }
227
+ input["label"] = node.attributes[:label] if node.attributes[:label]
228
+ input["style"] = "compact"
229
+ input["isRequired"] = !node.attributes[:optional]
230
+ end
231
+ input["value"] = node.attributes[:initial_option] unless node.attributes[:initial_option].nil?
232
+ input
233
+ end
234
+
235
+ def render_radio_select(node)
236
+ input = {
237
+ "type" => "Input.ChoiceSet",
238
+ "id" => node.attributes[:id],
239
+ "label" => node.attributes[:label],
240
+ "style" => "expanded",
241
+ "isRequired" => !node.attributes[:optional],
242
+ "choices" => node.children.map do |option|
243
+ {
244
+ "title" => option.attributes[:text],
245
+ "value" => option.attributes[:value]
246
+ }
247
+ end
248
+ }
249
+ input["value"] = node.attributes[:initial_option] unless node.attributes[:initial_option].nil?
181
250
  input
182
251
  end
183
252
 
@@ -191,6 +260,7 @@ module ChatSDK
191
260
  }
192
261
  input["placeholder"] = node.attributes[:placeholder] if node.attributes[:placeholder]
193
262
  input["value"] = node.attributes[:value] unless node.attributes[:value].nil?
263
+ input["maxLength"] = node.attributes[:max_length] unless node.attributes[:max_length].nil?
194
264
  input
195
265
  end
196
266
 
@@ -8,6 +8,8 @@ require "zeitwerk"
8
8
 
9
9
  module ChatSDK
10
10
  module Teams
11
+ AUTO_SUBMIT_ACTION_ID = "__auto_submit"
12
+
11
13
  class << self
12
14
  def loader
13
15
  @loader ||= begin
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chat_sdk-teams
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Quentin Rousseau
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '1.2'
18
+ version: '1.3'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '1.2'
25
+ version: '1.3'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: faraday
28
28
  requirement: !ruby/object:Gem::Requirement