chat_sdk 1.1.0 → 1.2.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: 36c07ba6e29c7d1cf858767caca271826418d829547426f1677a32c94133205e
4
- data.tar.gz: 0a00e35abebb7f43b8c9bb68576fce634d46088430029b33ad7f63c2d3820bbb
3
+ metadata.gz: dc45b8149b772db87ae0793f070a3f068353ef09a177d59c3526eb0ec37d1fb9
4
+ data.tar.gz: 16eb68d587eb53a43337f22dd2cd7d5b564383d892ecda9b64b77b5ae45a05a0
5
5
  SHA512:
6
- metadata.gz: 5483b777f81c28e3e367f84d0e4a3413b8f7d60ff09036d6148a3e565a5287252660e0fcc2aa3e7cce6c0d2f607f7a4834514f7f4da66445c1a6728ebc1a0ff6
7
- data.tar.gz: b2650d632171a42556aab7d6df09d5b6c79e7cfd1a032b9806a5d49b65892c3c62435d22463a24d382904df03c02b81eb3e93d5f86b0b83570e5ff24f9bb88a8
6
+ metadata.gz: 7a6cb0121846bf8a75b849f0d6bedfc48601c738dba99c3ad838ecdcb3b31fd7b4a27cd16bfa75cee66a4f70e62b62ae1c4730546b22f430e72d1af1bf6219c2
7
+ data.tar.gz: 602d33c5bba332dac427d61e8566c396f10c057e6b17a3422d1ab4846e4d7e8857ece123b50d2d357e310072f119bee00436db593e6138e46aabc34ba65f777c
@@ -22,6 +22,10 @@ module ChatSDK
22
22
  nil
23
23
  end
24
24
 
25
+ def webhook_response(_events)
26
+ nil
27
+ end
28
+
25
29
  def parse_events(rack_request)
26
30
  raise NotImplementedError
27
31
  end
@@ -41,12 +45,12 @@ module ChatSDK
41
45
  raise NotImplementedError
42
46
  end
43
47
 
44
- def edit_message(channel_id:, message_id:, message:)
48
+ def edit_message(channel_id:, message_id:, message:, thread_id: nil)
45
49
  require_capability!(:edit_messages)
46
50
  raise NotImplementedError
47
51
  end
48
52
 
49
- def delete_message(channel_id:, message_id:)
53
+ def delete_message(channel_id:, message_id:, thread_id: nil)
50
54
  require_capability!(:delete_messages)
51
55
  raise NotImplementedError
52
56
  end
@@ -61,12 +65,12 @@ module ChatSDK
61
65
  raise NotImplementedError
62
66
  end
63
67
 
64
- def add_reaction(channel_id:, message_id:, emoji:)
68
+ def add_reaction(channel_id:, message_id:, emoji:, thread_id: nil)
65
69
  require_capability!(:reactions)
66
70
  raise NotImplementedError
67
71
  end
68
72
 
69
- def remove_reaction(channel_id:, message_id:, emoji:)
73
+ def remove_reaction(channel_id:, message_id:, emoji:, thread_id: nil)
70
74
  require_capability!(:reactions)
71
75
  raise NotImplementedError
72
76
  end
@@ -96,11 +100,15 @@ module ChatSDK
96
100
  raise NotImplementedError
97
101
  end
98
102
 
99
- def start_typing(channel_id:, thread_id: nil)
103
+ def start_typing(channel_id:, thread_id: nil, message_id: nil)
100
104
  require_capability!(:typing_indicator)
101
105
  raise NotImplementedError
102
106
  end
103
107
 
108
+ def concurrency_scope(_event)
109
+ nil
110
+ end
111
+
104
112
  def mention(user_id)
105
113
  raise NotImplementedError
106
114
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Adapter
5
+ module Invocation
6
+ private
7
+
8
+ def call_adapter(method_name, **kwargs)
9
+ params = adapter.method(method_name).parameters
10
+ accepts_thread_id = params.any? do |type, name|
11
+ type == :keyrest || (%i[key keyreq].include?(type) && name == :thread_id)
12
+ end
13
+ kwargs.delete(:thread_id) unless accepts_thread_id
14
+ adapter.public_send(method_name, **kwargs)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -4,29 +4,39 @@ module ChatSDK
4
4
  module AI
5
5
  module ConversationScope
6
6
  THREAD_KEY = :chat_sdk_ai_conversation_scope
7
+ ACTIVE_KEY = :chat_sdk_ai_conversation_scope_active
7
8
 
8
9
  class << self
9
10
  def current
10
11
  ::Thread.current[THREAD_KEY]
11
12
  end
12
13
 
14
+ def active?
15
+ !!::Thread.current[ACTIVE_KEY]
16
+ end
17
+
13
18
  def with(thread)
14
19
  previous = current
20
+ previous_active = active?
15
21
  ::Thread.current[THREAD_KEY] = scope_for(thread)
22
+ ::Thread.current[ACTIVE_KEY] = true
16
23
  yield
17
24
  ensure
18
25
  ::Thread.current[THREAD_KEY] = previous
26
+ ::Thread.current[ACTIVE_KEY] = previous_active
19
27
  end
20
28
 
21
29
  def scope_for(value)
22
30
  return if value.nil?
23
31
  return value.transform_keys(&:to_sym) if value.is_a?(Hash)
24
32
 
25
- {
33
+ scope = {
26
34
  adapter_name: value.adapter.name.to_sym,
27
35
  channel_id: value.respond_to?(:channel_id) ? value.channel_id.to_s : value.id.to_s,
28
36
  thread_id: value.is_a?(ChatSDK::Thread) ? value.id.to_s : nil
29
37
  }
38
+ scope[:message_id] = value.current_message.id if value.respond_to?(:current_message) && value.current_message
39
+ scope
30
40
  end
31
41
  end
32
42
  end
@@ -131,7 +131,8 @@ module ChatSDK
131
131
  properties: {
132
132
  adapter_name: {type: "string", description: "Adapter name"},
133
133
  channel_id: {type: "string", description: "Channel ID"},
134
- thread_id: {type: "string", description: "Thread ID (optional)"}
134
+ thread_id: {type: "string", description: "Thread ID (optional)"},
135
+ message_id: {type: "string", description: "Current message ID (optional)"}
135
136
  },
136
137
  required: %w[adapter_name channel_id]
137
138
  },
@@ -5,13 +5,11 @@ module ChatSDK
5
5
  class ToolExecutor
6
6
  def initialize(chat:, scope: nil, strict_scope: false)
7
7
  @chat = chat
8
- @scope = if scope == false
9
- false
10
- else
11
- ConversationScope.scope_for(scope) || ConversationScope.current&.dup
12
- end
8
+ @scope = (scope == false) ? false : (ConversationScope.scope_for(scope) || ConversationScope.current&.dup)
9
+ @created_in_handler = ConversationScope.active?
10
+ validate_scope!(@scope) if @scope
13
11
  @strict_scope = strict_scope
14
- @warned_unscoped = false
12
+ @warned_unscoped = {}
15
13
  end
16
14
 
17
15
  def execute(tool_name, arguments)
@@ -20,7 +18,7 @@ module ChatSDK
20
18
 
21
19
  args = arguments.transform_keys(&:to_sym)
22
20
  adapter_name = args[:adapter_name].to_sym
23
- guard_scope!(adapter_name, args) unless tool_name == :send_direct_message
21
+ guard_scope!(tool_name, adapter_name, args)
24
22
 
25
23
  send(:"execute_#{tool_name}", adapter_name, args)
26
24
  end
@@ -48,24 +46,25 @@ module ChatSDK
48
46
 
49
47
  def execute_post_message(adapter_name, args)
50
48
  channel = @chat.channel(args[:channel_id], adapter_name: adapter_name)
49
+ message = ChatSDK::PostableMessage.new(text: args[:text], ai_generated: true)
51
50
  if args[:thread_id]
52
51
  thread = channel.thread(args[:thread_id])
53
- result = thread.post(args[:text])
52
+ result = thread.post(message)
54
53
  else
55
- result = channel.post(args[:text])
54
+ result = channel.post(message)
56
55
  end
57
56
  {id: result.id, text: args[:text]}
58
57
  end
59
58
 
60
59
  def execute_send_direct_message(adapter_name, args)
61
60
  dm_channel = @chat.open_dm(args[:user_id], adapter_name: adapter_name)
62
- result = dm_channel.post(args[:text])
61
+ result = dm_channel.post(ChatSDK::PostableMessage.new(text: args[:text], ai_generated: true))
63
62
  {id: result.id, channel_id: dm_channel.id}
64
63
  end
65
64
 
66
65
  def execute_edit_message(adapter_name, args)
67
66
  thread = target_thread(adapter_name, args)
68
- thread.edit(args[:message_id], args[:text])
67
+ thread.edit(args[:message_id], ChatSDK::PostableMessage.new(text: args[:text], ai_generated: true))
69
68
  {success: true}
70
69
  end
71
70
 
@@ -89,7 +88,9 @@ module ChatSDK
89
88
 
90
89
  def execute_start_typing(adapter_name, args)
91
90
  adapter = @chat.adapter(adapter_name)
92
- adapter.start_typing(channel_id: args[:channel_id], thread_id: args[:thread_id])
91
+ scoped_message_id = @scope.is_a?(Hash) ? @scope[:message_id] : nil
92
+ message_id = args[:message_id] || ConversationScope.current&.[](:message_id) || scoped_message_id
93
+ adapter.start_typing(channel_id: args[:channel_id], thread_id: args[:thread_id], message_id: message_id)
93
94
  {success: true}
94
95
  end
95
96
 
@@ -102,18 +103,25 @@ module ChatSDK
102
103
  channel.thread(args[:thread_id] || args[:channel_id])
103
104
  end
104
105
 
105
- def guard_scope!(adapter_name, args)
106
+ def guard_scope!(tool_name, adapter_name, args)
106
107
  return if @scope == false
107
108
 
108
109
  scope = @scope || ConversationScope.current
109
110
  unless scope
110
- unless @warned_unscoped
111
+ if ConversationScope.active? || @created_in_handler
112
+ raise ChatSDK::Error, "AI tool call requires an explicit scope when run inside a handler without a conversation"
113
+ end
114
+
115
+ unless @warned_unscoped[tool_name]
111
116
  ChatSDK::Log.warn("AI tool ran without a conversation scope; pass scope: to create_executor to confine access")
112
- @warned_unscoped = true
117
+ @warned_unscoped[tool_name] = true
113
118
  end
119
+
114
120
  return
115
121
  end
116
122
 
123
+ return if tool_name == :send_direct_message
124
+
117
125
  target_channel = args[:channel_id].to_s
118
126
  target_thread = args[:thread_id]&.to_s
119
127
  same_channel = adapter_name == scope.fetch(:adapter_name).to_sym && target_channel == scope.fetch(:channel_id).to_s
@@ -125,6 +133,15 @@ module ChatSDK
125
133
  active = [scope[:adapter_name], scope[:channel_id], scope[:thread_id]].compact.join(":")
126
134
  raise ChatSDK::Error, "AI tool call blocked: executor is scoped to #{active.inspect}, but targeted #{target.inspect}"
127
135
  end
136
+
137
+ def validate_scope!(scope)
138
+ %i[channel_id thread_id].each do |key|
139
+ next if key == :thread_id && scope[key].nil?
140
+ next unless scope.key?(key) && scope[key].to_s.empty?
141
+
142
+ raise ChatSDK::ConfigurationError, "AI tool scope #{key} must not be empty"
143
+ end
144
+ end
128
145
  end
129
146
  end
130
147
  end
@@ -4,11 +4,12 @@ module ChatSDK
4
4
  class Author
5
5
  attr_reader :id, :name, :platform, :locale, :email, :raw
6
6
 
7
- def initialize(id:, name:, platform:, bot: false, system: false, locale: nil, email: nil, raw: nil)
7
+ def initialize(id:, name:, platform:, bot: false, me: false, system: false, locale: nil, email: nil, raw: nil)
8
8
  @id = id
9
9
  @name = name
10
10
  @platform = platform
11
11
  @bot = bot
12
+ @me = !!me
12
13
  @system = system
13
14
  @locale = locale
14
15
  @email = email
@@ -19,6 +20,10 @@ module ChatSDK
19
20
  @bot
20
21
  end
21
22
 
23
+ def me?
24
+ @me
25
+ end
26
+
22
27
  def system?
23
28
  @system
24
29
  end
@@ -9,18 +9,20 @@ module ChatSDK
9
9
  @nodes = []
10
10
  end
11
11
 
12
- def button(text, id:, style: nil, value: nil, tooltip: nil)
12
+ def button(text, id:, style: nil, value: nil, tooltip: nil, visible_to: nil)
13
13
  attrs = {text: text, id: id}
14
14
  attrs[:style] = style if style
15
15
  attrs[:value] = value if value
16
16
  attrs[:tooltip] = tooltip if tooltip
17
+ attrs[:visible_to] = visible_to if visible_to
17
18
  @nodes << Node.new(:button, attributes: attrs)
18
19
  end
19
20
 
20
- def link_button(text, url:, id: nil, tooltip: nil)
21
+ def link_button(text, url:, id: nil, tooltip: nil, visible_to: nil)
21
22
  attrs = {text: text, url: url}
22
23
  attrs[:id] = id if id
23
24
  attrs[:tooltip] = tooltip if tooltip
25
+ attrs[:visible_to] = visible_to if visible_to
24
26
  @nodes << Node.new(:link_button, attributes: attrs)
25
27
  end
26
28
 
@@ -3,6 +3,8 @@
3
3
  module ChatSDK
4
4
  module Cards
5
5
  class Builder
6
+ RESERVED_TEXT_INPUT_IDS = %w[actionId value msteams].freeze
7
+
6
8
  def initialize(title: nil, subtitle: nil, width: nil, &block)
7
9
  @title = title
8
10
  @subtitle = subtitle
@@ -31,6 +33,17 @@ module ChatSDK
31
33
  @children << Node.new(:image, attributes: {url: url, alt: alt})
32
34
  end
33
35
 
36
+ def text_input(id:, label:, placeholder: nil, multiline: false, optional: false, value: nil)
37
+ if RESERVED_TEXT_INPUT_IDS.include?(id.to_s)
38
+ raise ArgumentError, "Text input id #{id.inspect} is reserved"
39
+ end
40
+
41
+ attrs = {id: id, label: label, multiline: multiline, optional: optional}
42
+ attrs[:placeholder] = placeholder if placeholder
43
+ attrs[:value] = value unless value.nil?
44
+ @children << Node.new(:text_input, attributes: attrs)
45
+ end
46
+
34
47
  def fields(&block)
35
48
  ctx = FieldsContext.new
36
49
  ctx.instance_eval(&block)
@@ -50,11 +63,15 @@ module ChatSDK
50
63
  @children << Node.new(:actions, children: ctx.nodes)
51
64
  end
52
65
 
53
- def table(headers:, rows:, align: nil, caption: nil, page_size: nil)
66
+ def table(headers:, rows:, align: nil, caption: nil, page_size: nil, widths: nil, grid_lines: nil, grid_style: nil, vertical_align: nil)
54
67
  attrs = {headers: headers, rows: rows}
55
68
  attrs[:align] = align if align
56
69
  attrs[:caption] = caption if caption
57
70
  attrs[:page_size] = page_size if page_size
71
+ attrs[:widths] = widths if widths
72
+ attrs[:grid_lines] = grid_lines unless grid_lines.nil?
73
+ attrs[:grid_style] = grid_style if grid_style
74
+ attrs[:vertical_align] = vertical_align if vertical_align
58
75
  @children << Node.new(:table, attributes: attrs)
59
76
  end
60
77
 
@@ -31,6 +31,8 @@ module ChatSDK
31
31
  ["#{node.attributes[:label]}: #{node.attributes[:value]}"]
32
32
  when :button, :link_button
33
33
  [node.attributes[:text]]
34
+ when :text_input
35
+ [node.attributes[:label]]
34
36
  when :table
35
37
  table_text(node)
36
38
  when :chart
@@ -15,6 +15,7 @@ module ChatSDK
15
15
  when :actions then render_actions(node)
16
16
  when :button then "[#{node.attributes[:text]}]"
17
17
  when :link_button then "[#{node.attributes[:text]}](#{node.attributes[:url]})"
18
+ when :text_input then node.attributes[:label]
18
19
  when :select then "_#{node.attributes[:placeholder] || "Select"}_"
19
20
  when :table then render_table(node)
20
21
  when :chart then render_chart(node)
data/lib/chat_sdk/chat.rb CHANGED
@@ -8,6 +8,7 @@ module ChatSDK
8
8
  @config = Config.new(user_name: user_name, adapters: adapters, state: state, **options)
9
9
  @state = state
10
10
  @adapters = adapters
11
+ @adapters.each_value { |adapter| adapter.set_state(state) if adapter.respond_to?(:set_state) }
11
12
  @registry = EventRegistry.new
12
13
  @history = History.new(self)
13
14
  @webhooks = {}
@@ -26,7 +27,12 @@ module ChatSDK
26
27
  end
27
28
 
28
29
  def on_new_message(pattern = nil, &block)
29
- @registry.register(:mention, matcher: pattern, &block)
30
+ unless pattern.nil? || pattern.is_a?(Regexp)
31
+ raise ChatSDK::ConfigurationError, "on_new_message pattern must be a Regexp or nil"
32
+ end
33
+ raise ChatSDK::ConfigurationError, "on_new_message requires a block" unless block
34
+
35
+ @registry.register(:new_message, matcher: pattern, &block)
30
36
  end
31
37
 
32
38
  def on_direct_message(&block)
@@ -53,6 +59,14 @@ module ChatSDK
53
59
  @registry.register(:slash_command, matcher: command, &block)
54
60
  end
55
61
 
62
+ def on_installed(&block)
63
+ @registry.register(:installed, &block)
64
+ end
65
+
66
+ def on_uninstalled(&block)
67
+ @registry.register(:uninstalled, &block)
68
+ end
69
+
56
70
  # Adapter access
57
71
  def adapter(name)
58
72
  @adapters.fetch(name) { raise ChatSDK::ConfigurationError, "Unknown adapter: #{name}" }
@@ -27,10 +27,11 @@ module ChatSDK
27
27
  return execute_event(event, build_thread(event, adapter))
28
28
  end
29
29
 
30
- return unless dedupe(event, adapter_name)
30
+ concurrency_scope = adapter.concurrency_scope(event)
31
+ return unless dedupe(event, adapter_name, concurrency_scope)
31
32
 
32
33
  thread = build_thread(event, adapter)
33
- thread_key = thread_key_for(event, adapter_name)
34
+ thread_key = thread_key_for(event, adapter_name, concurrency_scope)
34
35
  strategy = @config.concurrency[:strategy]
35
36
 
36
37
  if strategy == :concurrent
@@ -155,12 +156,12 @@ module ChatSDK
155
156
  end
156
157
  end
157
158
 
158
- def dedupe(event, adapter_name)
159
+ def dedupe(event, adapter_name, concurrency_scope)
159
160
  event_id = extract_event_id(event)
160
161
  return true unless event_id
161
162
 
162
163
  is_new = @state.set_if_absent(
163
- "chat_sdk:dedupe:#{adapter_name}:#{event_id}",
164
+ dedupe_key(adapter_name, concurrency_scope, event_id),
164
165
  true,
165
166
  ttl: @config.dedupe_ttl
166
167
  )
@@ -178,12 +179,18 @@ module ChatSDK
178
179
  end
179
180
  end
180
181
 
181
- def thread_key_for(event, adapter_name)
182
+ def dedupe_key(adapter_name, concurrency_scope, event_id)
183
+ prefix = ["chat_sdk:dedupe", adapter_name]
184
+ prefix << concurrency_scope if concurrency_scope
185
+ "#{prefix.join(":")}:#{event_id}"
186
+ end
187
+
188
+ def thread_key_for(event, adapter_name, concurrency_scope)
182
189
  channel_id = event.respond_to?(:channel_id) ? event.channel_id : nil
183
190
  thread_id = event.respond_to?(:thread_id) ? event.thread_id : nil
184
191
  lock_scope = @config.concurrency[:lock_scope]
185
192
  scope_id = (lock_scope&.to_sym == :channel) ? channel_id : thread_id
186
- "#{adapter_name}:#{channel_id}:#{scope_id}"
193
+ [adapter_name, concurrency_scope, channel_id, scope_id].compact.join(":")
187
194
  end
188
195
 
189
196
  def acquire_lock(thread_key, event, owner, force: false, apply_conflict_policy: true)
@@ -256,20 +263,67 @@ module ChatSDK
256
263
  end
257
264
 
258
265
  def execute_event(event, thread, context: nil)
266
+ if %i[mention subscribed_message direct_message].include?(event.type)
267
+ route_message_event(event, thread, context)
268
+ return
269
+ end
270
+
259
271
  @registry.handlers_for(event).each do |handler|
260
272
  execute_handler(handler, event, thread, context: context)
261
273
  end
262
274
  end
263
275
 
276
+ def route_message_event(event, thread, context)
277
+ message = event.message
278
+ dm_handlers = @registry.handlers_for_type(:direct_message, event: event)
279
+ if event.type == :direct_message && dm_handlers.any?
280
+ run_handlers(dm_handlers, event, thread, context)
281
+ return
282
+ end
283
+
284
+ message.is_mention = true if event.type == :direct_message
285
+
286
+ if thread&.subscribed?
287
+ run_handlers(@registry.handlers_for_type(:subscribed_message, event: event), event, thread, context)
288
+ return
289
+ end
290
+
291
+ if message.is_mention.nil?
292
+ message.is_mention = if event.type == :mention
293
+ true
294
+ else
295
+ detect_mention(message.text)
296
+ end
297
+ end
298
+
299
+ if message.is_mention
300
+ run_handlers(@registry.handlers_for_type(:mention, event: event), event, thread, context)
301
+ return
302
+ end
303
+
304
+ @registry.handlers_for_type(:new_message, event: event).each do |handler|
305
+ execute_handler(handler, event, thread, context: context)
306
+ end
307
+ end
308
+
309
+ def run_handlers(handlers, event, thread, context)
310
+ handlers.each { |handler| execute_handler(handler, event, thread, context: context) }
311
+ end
312
+
313
+ def detect_mention(text)
314
+ masked_text = ChatSDK::Format::Mentions.mask_code_spans(text)
315
+ /(?<![[:word:]])@#{Regexp.escape(@config.user_name)}\b/i.match?(masked_text)
316
+ end
317
+
264
318
  def execute_handler(handler, event, thread, context: nil)
265
319
  ChatSDK::Instrumentation.instrument("handler.chat_sdk", handler_type: event.type) do
266
320
  ChatSDK::AI::ConversationScope.with(thread) do
267
321
  case event.type
268
- when :mention, :subscribed_message, :direct_message
322
+ when :mention, :subscribed_message, :direct_message, :new_message
269
323
  handler.block.call(thread, event.message, context)
270
324
  when :message_updated
271
325
  handler.block.call(thread, event.message, event.previous_message)
272
- when :reaction, :action, :slash_command, :message_deleted
326
+ when :reaction, :action, :slash_command, :message_deleted, :installed, :uninstalled
273
327
  add_thread_to_event(event, thread)
274
328
  handler.block.call(event)
275
329
  end
@@ -13,13 +13,18 @@ module ChatSDK
13
13
  end
14
14
 
15
15
  def handlers_for(event)
16
- @handlers.select { |h| matches?(h, event) }
16
+ handlers_for_type(event.type, event: event)
17
+ end
18
+
19
+ def handlers_for_type(type, event: nil)
20
+ @handlers.select do |handler|
21
+ handler.type == type && (!event || matches?(handler, event))
22
+ end
17
23
  end
18
24
 
19
25
  private
20
26
 
21
27
  def matches?(handler, event)
22
- return false unless handler.type == event.type
23
28
  return true if handler.matcher.nil?
24
29
 
25
30
  case handler.matcher
@@ -21,9 +21,18 @@ module ChatSDK
21
21
  when :reaction
22
22
  data.merge!("emoji" => event.emoji, "user_id" => event.user_id, "message_id" => event.message_id, "added" => event.added?)
23
23
  when :action
24
- data.merge!("action_id" => event.action_id, "value" => event.value, "user" => dump_author(event.user), "trigger_id" => event.trigger_id)
24
+ data.merge!("action_id" => event.action_id, "value" => event.value, "values" => json_safe(event.values), "user" => dump_author(event.user), "trigger_id" => event.trigger_id)
25
25
  when :slash_command
26
26
  data.merge!("command" => event.command, "text" => event.text, "user_id" => event.user_id, "trigger_id" => event.trigger_id)
27
+ when :installed, :uninstalled
28
+ data.merge!(
29
+ "installation_id" => event.installation_id,
30
+ "team_id" => event.team_id,
31
+ "tenant_id" => event.tenant_id,
32
+ "conversation_id" => event.conversation_id,
33
+ "action" => event.action,
34
+ "user" => dump_author(event.user)
35
+ )
27
36
  end
28
37
  data
29
38
  end
@@ -55,10 +64,27 @@ module ChatSDK
55
64
  when :reaction
56
65
  Events::Reaction.new(emoji: data["emoji"], user_id: data["user_id"], message_id: data["message_id"], added: data["added"], **common)
57
66
  when :action
58
- Events::Action.new(action_id: data["action_id"], value: data["value"], user: load_author(data["user"]), trigger_id: data["trigger_id"], **common)
67
+ Events::Action.new(action_id: data["action_id"], value: data["value"], values: data["values"], user: load_author(data["user"]), trigger_id: data["trigger_id"], **common)
59
68
  when :slash_command
60
69
  common.delete(:thread_id)
61
70
  Events::SlashCommand.new(command: data["command"], text: data["text"], user_id: data["user_id"], trigger_id: data["trigger_id"], **common)
71
+ when :installed, :uninstalled
72
+ common.delete(:thread_id)
73
+ common.delete(:channel_id)
74
+ event_class = if type == :installed
75
+ Events::Installed
76
+ else
77
+ Events::Uninstalled
78
+ end
79
+ event_class.new(
80
+ installation_id: data["installation_id"],
81
+ team_id: data["team_id"],
82
+ tenant_id: data["tenant_id"],
83
+ conversation_id: data["conversation_id"],
84
+ action: data["action"],
85
+ user: load_author(data["user"]),
86
+ **common
87
+ )
62
88
  else
63
89
  raise ArgumentError, "unsupported queued event type: #{type}"
64
90
  end
@@ -93,7 +119,8 @@ module ChatSDK
93
119
  "reply_to" => dump_message(message.reply_to),
94
120
  "subject" => json_safe(message.subject),
95
121
  "raw" => json_safe(message.raw),
96
- "timestamp" => message.timestamp&.iso8601
122
+ "timestamp" => message.timestamp&.iso8601,
123
+ "is_mention" => message.is_mention
97
124
  }
98
125
  end
99
126
 
@@ -113,7 +140,8 @@ module ChatSDK
113
140
  reply_to: load_message(data["reply_to"]),
114
141
  subject: data["subject"],
115
142
  raw: data["raw"],
116
- timestamp: parse_time(data["timestamp"])
143
+ timestamp: parse_time(data["timestamp"]),
144
+ is_mention: data["is_mention"]
117
145
  )
118
146
  end
119
147
 
@@ -125,6 +153,7 @@ module ChatSDK
125
153
  "name" => author.name,
126
154
  "platform" => author.platform.to_s,
127
155
  "bot" => author.bot?,
156
+ "me" => author.me?,
128
157
  "system" => author.system?,
129
158
  "locale" => author.locale,
130
159
  "email" => author.email,
@@ -141,6 +170,7 @@ module ChatSDK
141
170
  name: data["name"],
142
171
  platform: data["platform"].to_sym,
143
172
  bot: data["bot"],
173
+ me: data["me"],
144
174
  system: data["system"],
145
175
  locale: data["locale"],
146
176
  email: data["email"],
@@ -3,13 +3,14 @@
3
3
  module ChatSDK
4
4
  module Events
5
5
  class Action < Base
6
- attr_reader :action_id, :value, :user, :thread_id, :channel_id, :trigger_id
6
+ attr_reader :action_id, :value, :values, :user, :thread_id, :channel_id, :trigger_id
7
7
  attr_accessor :thread
8
8
 
9
- def initialize(action_id:, thread_id:, channel_id:, value: nil, user: nil, trigger_id: nil, **kwargs)
9
+ def initialize(action_id:, thread_id:, channel_id:, value: nil, values: nil, user: nil, trigger_id: nil, **kwargs)
10
10
  super(type: :action, **kwargs)
11
11
  @action_id = action_id
12
12
  @value = value
13
+ @values = values
13
14
  @user = user
14
15
  @thread_id = thread_id
15
16
  @channel_id = channel_id
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Events
5
+ class Installed < Base
6
+ attr_reader :installation_id, :team_id, :tenant_id, :conversation_id, :action, :user
7
+
8
+ def initialize(conversation_id:, action:, installation_id: nil, team_id: nil, tenant_id: nil, user: nil, **kwargs)
9
+ super(type: :installed, **kwargs)
10
+ @installation_id = installation_id
11
+ @team_id = team_id
12
+ @tenant_id = tenant_id
13
+ @conversation_id = conversation_id
14
+ @action = action
15
+ @user = user
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Events
5
+ class Uninstalled < Base
6
+ attr_reader :installation_id, :team_id, :tenant_id, :conversation_id, :action, :user
7
+
8
+ def initialize(conversation_id:, action:, installation_id: nil, team_id: nil, tenant_id: nil, user: nil, **kwargs)
9
+ super(type: :uninstalled, **kwargs)
10
+ @installation_id = installation_id
11
+ @team_id = team_id
12
+ @tenant_id = tenant_id
13
+ @conversation_id = conversation_id
14
+ @action = action
15
+ @user = user
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Format
5
+ module Mentions
6
+ class << self
7
+ def mask_code_spans(text)
8
+ characters = text.to_s.chars
9
+ masked = characters.dup
10
+ opening = nil
11
+ opening_length = nil
12
+ fence = false
13
+ index = 0
14
+
15
+ while index < characters.length
16
+ if characters[index] == "`"
17
+ finish = index + 1
18
+ finish += 1 while finish < characters.length && characters[finish] == "`"
19
+ length = finish - index
20
+
21
+ if opening
22
+ closes = fence ? length >= 3 : length == opening_length
23
+ if closes
24
+ blank_span(masked, opening, finish)
25
+ opening = nil
26
+ opening_length = nil
27
+ fence = false
28
+ else
29
+ blank_span(masked, index, finish)
30
+ end
31
+ else
32
+ opening = index
33
+ opening_length = length
34
+ fence = length >= 3
35
+ blank_span(masked, index, finish)
36
+ end
37
+
38
+ index = finish
39
+ else
40
+ if opening
41
+ masked[index] = " " unless characters[index] == "\n" || characters[index] == "\r"
42
+ end
43
+ index += 1
44
+ end
45
+ end
46
+
47
+ masked[opening..] = characters[opening..] if opening
48
+ masked.join
49
+ end
50
+
51
+ private
52
+
53
+ def blank_span(characters, first, finish)
54
+ (first...finish).each do |index|
55
+ characters[index] = " " unless characters[index] == "\n" || characters[index] == "\r"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -4,9 +4,11 @@ module ChatSDK
4
4
  class Message
5
5
  attr_reader :id, :text, :author, :thread_id, :channel_id,
6
6
  :platform, :attachments, :links, :reply_to, :subject, :raw, :timestamp
7
+ attr_accessor :is_mention
7
8
 
8
9
  def initialize(id:, text:, author:, thread_id:, channel_id:, platform:,
9
- attachments: [], links: [], reply_to: nil, subject: nil, raw: nil, timestamp: nil)
10
+ attachments: [], links: [], reply_to: nil, subject: nil, raw: nil,
11
+ timestamp: nil, is_mention: nil)
10
12
  @id = id
11
13
  @text = text
12
14
  @author = author
@@ -19,6 +21,7 @@ module ChatSDK
19
21
  @subject = subject
20
22
  @raw = raw
21
23
  @timestamp = timestamp
24
+ @is_mention = is_mention
22
25
  end
23
26
 
24
27
  def ==(other)
@@ -2,14 +2,15 @@
2
2
 
3
3
  module ChatSDK
4
4
  class PostableMessage
5
- attr_reader :text, :card, :attachments, :metadata
5
+ attr_reader :text, :card, :attachments, :metadata, :ai_generated
6
6
 
7
- def initialize(text: nil, card: nil, attachments: [], metadata: {})
7
+ def initialize(text: nil, card: nil, attachments: [], metadata: {}, ai_generated: false)
8
8
  raise ArgumentError, "text or card required" if text.nil? && card.nil?
9
9
  @text = text
10
10
  @card = card
11
11
  @attachments = attachments
12
12
  @metadata = metadata
13
+ @ai_generated = ai_generated
13
14
  end
14
15
 
15
16
  def card?
@@ -3,6 +3,8 @@
3
3
  module ChatSDK
4
4
  module Streaming
5
5
  class Stream
6
+ include ChatSDK::Adapter::Invocation
7
+
6
8
  attr_reader :adapter, :channel_id, :thread_id, :update_interval
7
9
 
8
10
  def initialize(adapter:, channel_id:, thread_id:, placeholder: nil, update_interval: 0.5)
@@ -50,10 +52,12 @@ module ChatSDK
50
52
  return if @buffer.empty?
51
53
 
52
54
  if @message_id && adapter.supports?(:edit_messages)
53
- adapter.edit_message(
55
+ call_adapter(
56
+ :edit_message,
54
57
  channel_id: channel_id,
55
58
  message_id: @message_id,
56
- message: PostableMessage.new(text: @buffer.dup)
59
+ message: PostableMessage.new(text: @buffer.dup),
60
+ thread_id: thread_id
57
61
  )
58
62
  elsif !@message_id
59
63
  result = adapter.post_message(
@@ -50,7 +50,7 @@ module ChatSDK
50
50
  )
51
51
  end
52
52
 
53
- def edit_message(channel_id:, message_id:, message:)
53
+ def edit_message(channel_id:, message_id:, message:, **_options)
54
54
  record = RecordedCall.new(:edit_message, channel_id: channel_id, message_id: message_id, message: message)
55
55
  @edited_messages << record
56
56
  record
@@ -68,7 +68,7 @@ module ChatSDK
68
68
  record
69
69
  end
70
70
 
71
- def delete_message(channel_id:, message_id:)
71
+ def delete_message(channel_id:, message_id:, **_options)
72
72
  record = RecordedCall.new(:delete_message, channel_id: channel_id, message_id: message_id)
73
73
  @deleted_messages << record
74
74
  record
@@ -86,13 +86,13 @@ module ChatSDK
86
86
  record
87
87
  end
88
88
 
89
- def add_reaction(channel_id:, message_id:, emoji:)
89
+ def add_reaction(channel_id:, message_id:, emoji:, **_options)
90
90
  record = RecordedCall.new(:add_reaction, channel_id: channel_id, message_id: message_id, emoji: emoji)
91
91
  @reactions_added << record
92
92
  record
93
93
  end
94
94
 
95
- def remove_reaction(channel_id:, message_id:, emoji:)
95
+ def remove_reaction(channel_id:, message_id:, emoji:, **_options)
96
96
  record = RecordedCall.new(:remove_reaction, channel_id: channel_id, message_id: message_id, emoji: emoji)
97
97
  @reactions_removed << record
98
98
  record
@@ -122,8 +122,8 @@ module ChatSDK
122
122
  record
123
123
  end
124
124
 
125
- def start_typing(channel_id:, thread_id: nil)
126
- record = RecordedCall.new(:start_typing, channel_id: channel_id, thread_id: thread_id)
125
+ def start_typing(channel_id:, thread_id: nil, message_id: nil)
126
+ record = RecordedCall.new(:start_typing, channel_id: channel_id, thread_id: thread_id, message_id: message_id)
127
127
  @typing_started << record
128
128
  record
129
129
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ChatSDK
4
4
  class Thread
5
+ include Adapter::Invocation
6
+
5
7
  attr_reader :id, :channel_id, :adapter, :chat, :current_message
6
8
 
7
9
  def initialize(id:, channel_id:, adapter:, chat:, current_message: nil)
@@ -51,6 +53,10 @@ module ChatSDK
51
53
  adapter.mark_as_read(channel_id: channel_id, thread_id: id, message_id: message_id, message: target.is_a?(ChatSDK::Message) ? target : nil)
52
54
  end
53
55
 
56
+ def start_typing
57
+ adapter.start_typing(channel_id: channel_id, thread_id: id, message_id: current_message&.id)
58
+ end
59
+
54
60
  def post_ephemeral(content, user_id:)
55
61
  message = PostableMessage.from(content)
56
62
  adapter.post_ephemeral(channel_id: channel_id, user_id: user_id, message: message, thread_id: id)
@@ -69,19 +75,19 @@ module ChatSDK
69
75
 
70
76
  def edit(message_id, content)
71
77
  message = PostableMessage.from(content)
72
- adapter.edit_message(channel_id: channel_id, message_id: message_id, message: message)
78
+ call_adapter(:edit_message, channel_id: channel_id, message_id: message_id, message: message, thread_id: id)
73
79
  end
74
80
 
75
81
  def delete(message_id)
76
- adapter.delete_message(channel_id: channel_id, message_id: message_id)
82
+ call_adapter(:delete_message, channel_id: channel_id, message_id: message_id, thread_id: id)
77
83
  end
78
84
 
79
85
  def react(message_id, emoji)
80
- adapter.add_reaction(channel_id: channel_id, message_id: message_id, emoji: emoji)
86
+ call_adapter(:add_reaction, channel_id: channel_id, message_id: message_id, emoji: emoji, thread_id: id)
81
87
  end
82
88
 
83
89
  def unreact(message_id, emoji)
84
- adapter.remove_reaction(channel_id: channel_id, message_id: message_id, emoji: emoji)
90
+ call_adapter(:remove_reaction, channel_id: channel_id, message_id: message_id, emoji: emoji, thread_id: id)
85
91
  end
86
92
 
87
93
  def post_ai_stream(enumerable, placeholder: "Thinking...")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChatSDK
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
@@ -22,6 +22,9 @@ module ChatSDK
22
22
  events = adapter.parse_events(request || env)
23
23
  events.each { |event| chat.dispatch(event, adapter_name: adapter_name) }
24
24
 
25
+ response = adapter.webhook_response(events)
26
+ return response if response
27
+
25
28
  [200, {"content-type" => "text/plain"}, [""]]
26
29
  rescue ChatSDK::SignatureVerificationError => e
27
30
  ChatSDK::Log.warn("Signature verification failed: #{e.message}")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chat_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Quentin Rousseau
@@ -48,6 +48,7 @@ files:
48
48
  - lib/chat_sdk.rb
49
49
  - lib/chat_sdk/adapter/base.rb
50
50
  - lib/chat_sdk/adapter/capabilities.rb
51
+ - lib/chat_sdk/adapter/invocation.rb
51
52
  - lib/chat_sdk/adapter/media_types.rb
52
53
  - lib/chat_sdk/adapter/meta_verification.rb
53
54
  - lib/chat_sdk/ai.rb
@@ -75,13 +76,16 @@ files:
75
76
  - lib/chat_sdk/events/action.rb
76
77
  - lib/chat_sdk/events/base.rb
77
78
  - lib/chat_sdk/events/direct_message.rb
79
+ - lib/chat_sdk/events/installed.rb
78
80
  - lib/chat_sdk/events/mention.rb
79
81
  - lib/chat_sdk/events/message_deleted.rb
80
82
  - lib/chat_sdk/events/message_updated.rb
81
83
  - lib/chat_sdk/events/reaction.rb
82
84
  - lib/chat_sdk/events/slash_command.rb
83
85
  - lib/chat_sdk/events/subscribed_message.rb
86
+ - lib/chat_sdk/events/uninstalled.rb
84
87
  - lib/chat_sdk/format/converter.rb
88
+ - lib/chat_sdk/format/mentions.rb
85
89
  - lib/chat_sdk/history.rb
86
90
  - lib/chat_sdk/instrumentation.rb
87
91
  - lib/chat_sdk/log.rb