chat_sdk 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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/chat_sdk/adapter/base.rb +19 -5
  3. data/lib/chat_sdk/adapter/invocation.rb +18 -0
  4. data/lib/chat_sdk/ai/conversation_scope.rb +11 -1
  5. data/lib/chat_sdk/ai/stream_handler.rb +3 -3
  6. data/lib/chat_sdk/ai/tool_builder.rb +2 -1
  7. data/lib/chat_sdk/ai/tool_executor.rb +32 -15
  8. data/lib/chat_sdk/author.rb +6 -1
  9. data/lib/chat_sdk/cards/actions_context.rb +26 -4
  10. data/lib/chat_sdk/cards/builder.rb +30 -4
  11. data/lib/chat_sdk/cards/node.rb +4 -0
  12. data/lib/chat_sdk/cards/renderer.rb +3 -0
  13. data/lib/chat_sdk/chat.rb +80 -5
  14. data/lib/chat_sdk/config.rb +34 -2
  15. data/lib/chat_sdk/dispatcher.rb +107 -28
  16. data/lib/chat_sdk/error_reporter.rb +36 -0
  17. data/lib/chat_sdk/errors.rb +1 -0
  18. data/lib/chat_sdk/event_registry.rb +7 -2
  19. data/lib/chat_sdk/event_serializer.rb +66 -6
  20. data/lib/chat_sdk/events/action.rb +3 -2
  21. data/lib/chat_sdk/events/app_home_opened.rb +16 -0
  22. data/lib/chat_sdk/events/assistant_context.rb +25 -0
  23. data/lib/chat_sdk/events/assistant_context_changed.rb +18 -0
  24. data/lib/chat_sdk/events/assistant_thread_started.rb +18 -0
  25. data/lib/chat_sdk/events/installed.rb +19 -0
  26. data/lib/chat_sdk/events/member_joined_channel.rb +16 -0
  27. data/lib/chat_sdk/events/uninstalled.rb +19 -0
  28. data/lib/chat_sdk/format/mentions.rb +61 -0
  29. data/lib/chat_sdk/message.rb +4 -1
  30. data/lib/chat_sdk/modals/builder.rb +39 -4
  31. data/lib/chat_sdk/postable_message.rb +3 -2
  32. data/lib/chat_sdk/streaming/chunk.rb +35 -0
  33. data/lib/chat_sdk/streaming/markdown_text_chunk.rb +15 -0
  34. data/lib/chat_sdk/streaming/plan_update_chunk.rb +15 -0
  35. data/lib/chat_sdk/streaming/stream.rb +48 -8
  36. data/lib/chat_sdk/streaming/task_update_chunk.rb +26 -0
  37. data/lib/chat_sdk/streaming/writer.rb +16 -0
  38. data/lib/chat_sdk/streaming_plan.rb +32 -0
  39. data/lib/chat_sdk/testing/fake_adapter.rb +6 -6
  40. data/lib/chat_sdk/thread.rb +86 -15
  41. data/lib/chat_sdk/version.rb +1 -1
  42. data/lib/chat_sdk/webhook/endpoint.rb +45 -2
  43. data/lib/chat_sdk.rb +2 -2
  44. metadata +17 -1
@@ -5,17 +5,25 @@ require "time"
5
5
 
6
6
  module ChatSDK
7
7
  class Dispatcher
8
+ LIFECYCLE_EVENT_TYPES = %i[
9
+ assistant_thread_started
10
+ assistant_context_changed
11
+ app_home_opened
12
+ member_joined_channel
13
+ ].freeze
14
+
8
15
  LockHeartbeat = Struct.new(:thread, :mutex, :condition, :stop, :ownership_lost) do
9
16
  def ownership_lost?
10
17
  mutex.synchronize { ownership_lost }
11
18
  end
12
19
  end
13
20
 
14
- def initialize(chat:, config:, state:, registry:)
21
+ def initialize(chat:, config:, state:, registry:, error_reporter:)
15
22
  @chat = chat
16
23
  @config = config
17
24
  @state = state
18
25
  @registry = registry
26
+ @error_reporter = error_reporter
19
27
  @slot_mutex = Mutex.new
20
28
  @slot_condition = ConditionVariable.new
21
29
  @active_slots = Hash.new(0)
@@ -24,42 +32,49 @@ module ChatSDK
24
32
  def dispatch(event, adapter:, adapter_name:)
25
33
  ChatSDK::Instrumentation.instrument("dispatch.chat_sdk", adapter: adapter_name, event_type: event.type) do
26
34
  if %i[message_updated message_deleted].include?(event.type)
27
- return execute_event(event, build_thread(event, adapter))
35
+ return execute_event(event, build_thread(event, adapter), adapter_name: adapter_name)
36
+ end
37
+
38
+ if LIFECYCLE_EVENT_TYPES.include?(event.type)
39
+ return unless dedupe(event, adapter_name, adapter.concurrency_scope(event))
40
+
41
+ return execute_event(event, build_thread(event, adapter), adapter_name: adapter_name)
28
42
  end
29
43
 
30
- return unless dedupe(event, adapter_name)
44
+ concurrency_scope = adapter.concurrency_scope(event)
45
+ return unless dedupe(event, adapter_name, concurrency_scope)
31
46
 
32
47
  thread = build_thread(event, adapter)
33
- thread_key = thread_key_for(event, adapter_name)
48
+ thread_key = thread_key_for(event, adapter_name, concurrency_scope)
34
49
  strategy = @config.concurrency[:strategy]
35
50
 
36
51
  if strategy == :concurrent
37
- return with_concurrent_slot(thread_key) { execute_event(event, thread) }
52
+ return with_concurrent_slot(thread_key) { execute_event(event, thread, adapter_name: adapter_name) }
38
53
  end
39
54
 
40
55
  owner = "#{Process.pid}:#{::Thread.current.object_id}:#{SecureRandom.uuid}"
41
56
  if %i[queue burst debounce].include?(strategy)
42
- dispatch_queued(event, adapter, thread_key, owner, strategy)
57
+ dispatch_queued(event, adapter, adapter_name, thread_key, owner, strategy)
43
58
  else
44
- dispatch_locked(event, thread, thread_key, owner, strategy)
59
+ dispatch_locked(event, thread, adapter_name, thread_key, owner, strategy)
45
60
  end
46
61
  end
47
62
  end
48
63
 
49
64
  private
50
65
 
51
- def dispatch_locked(event, thread, thread_key, owner, strategy)
66
+ def dispatch_locked(event, thread, adapter_name, thread_key, owner, strategy)
52
67
  acquired = acquire_lock(thread_key, event, owner, force: strategy == :force)
53
68
  return unless acquired
54
69
 
55
70
  heartbeat = start_lock_heartbeat(thread_key, owner)
56
- execute_event(event, thread)
71
+ execute_event(event, thread, adapter_name: adapter_name)
57
72
  ensure
58
73
  stop_lock_heartbeat(heartbeat)
59
74
  release_lock(thread_key, owner) if acquired
60
75
  end
61
76
 
62
- def dispatch_queued(event, adapter, thread_key, owner, strategy)
77
+ def dispatch_queued(event, adapter, adapter_name, thread_key, owner, strategy)
63
78
  queue_key = "chat_sdk:queue:#{thread_key}"
64
79
  acquired = acquire_lock(thread_key, event, owner, apply_conflict_policy: false)
65
80
  pre_enqueued = false
@@ -73,23 +88,23 @@ module ChatSDK
73
88
 
74
89
  heartbeat = start_lock_heartbeat(thread_key, owner)
75
90
  if strategy == :queue
76
- execute_event(event, build_thread(event, adapter)) unless pre_enqueued
91
+ execute_event(event, build_thread(event, adapter), adapter_name: adapter_name) unless pre_enqueued
77
92
  else
78
93
  enqueue(queue_key, event) unless pre_enqueued
79
94
  sleep(@config.concurrency[:debounce].to_f) if strategy == :burst
80
95
  end
81
96
 
82
97
  if strategy == :debounce
83
- debounce_loop(queue_key, adapter, heartbeat)
98
+ debounce_loop(queue_key, adapter, adapter_name, heartbeat)
84
99
  else
85
- drain_loop(queue_key, adapter, strategy, heartbeat)
100
+ drain_loop(queue_key, adapter, adapter_name, strategy, heartbeat)
86
101
  end
87
102
  ensure
88
103
  stop_lock_heartbeat(heartbeat)
89
104
  release_lock(thread_key, owner) if acquired
90
105
  end
91
106
 
92
- def drain_loop(queue_key, adapter, strategy, heartbeat)
107
+ def drain_loop(queue_key, adapter, adapter_name, strategy, heartbeat)
93
108
  loop do
94
109
  break if heartbeat&.ownership_lost?
95
110
 
@@ -104,12 +119,12 @@ module ChatSDK
104
119
  events[0...-1].filter_map { |queued| queued.message if queued.respond_to?(:message) }
105
120
  end
106
121
  context = {skipped: skipped, total_since_last_handler: skipped.length + 1}
107
- execute_event(current, build_thread(current, adapter), context: context)
122
+ execute_event(current, build_thread(current, adapter), adapter_name: adapter_name, context: context)
108
123
  ::Thread.pass
109
124
  end
110
125
  end
111
126
 
112
- def debounce_loop(queue_key, adapter, heartbeat)
127
+ def debounce_loop(queue_key, adapter, adapter_name, heartbeat)
113
128
  skipped = []
114
129
  loop do
115
130
  sleep(@config.concurrency[:debounce].to_f)
@@ -131,7 +146,7 @@ module ChatSDK
131
146
  message.thread_id == latest.thread_id
132
147
  end
133
148
  context = {skipped: relevant, total_since_last_handler: relevant.length + 1}
134
- execute_event(latest, build_thread(latest, adapter), context: context)
149
+ execute_event(latest, build_thread(latest, adapter), adapter_name: adapter_name, context: context)
135
150
  skipped.clear
136
151
  end
137
152
  end
@@ -155,12 +170,12 @@ module ChatSDK
155
170
  end
156
171
  end
157
172
 
158
- def dedupe(event, adapter_name)
173
+ def dedupe(event, adapter_name, concurrency_scope)
159
174
  event_id = extract_event_id(event)
160
175
  return true unless event_id
161
176
 
162
177
  is_new = @state.set_if_absent(
163
- "chat_sdk:dedupe:#{adapter_name}:#{event_id}",
178
+ dedupe_key(adapter_name, concurrency_scope, event_id),
164
179
  true,
165
180
  ttl: @config.dedupe_ttl
166
181
  )
@@ -178,12 +193,18 @@ module ChatSDK
178
193
  end
179
194
  end
180
195
 
181
- def thread_key_for(event, adapter_name)
196
+ def dedupe_key(adapter_name, concurrency_scope, event_id)
197
+ prefix = ["chat_sdk:dedupe", adapter_name]
198
+ prefix << concurrency_scope if concurrency_scope
199
+ "#{prefix.join(":")}:#{event_id}"
200
+ end
201
+
202
+ def thread_key_for(event, adapter_name, concurrency_scope)
182
203
  channel_id = event.respond_to?(:channel_id) ? event.channel_id : nil
183
204
  thread_id = event.respond_to?(:thread_id) ? event.thread_id : nil
184
205
  lock_scope = @config.concurrency[:lock_scope]
185
206
  scope_id = (lock_scope&.to_sym == :channel) ? channel_id : thread_id
186
- "#{adapter_name}:#{channel_id}:#{scope_id}"
207
+ [adapter_name, concurrency_scope, channel_id, scope_id].compact.join(":")
187
208
  end
188
209
 
189
210
  def acquire_lock(thread_key, event, owner, force: false, apply_conflict_policy: true)
@@ -255,29 +276,87 @@ module ChatSDK
255
276
  ChatSDK::Thread.new(id: thread_id, channel_id: channel_id, adapter: adapter, chat: @chat, current_message: current_message)
256
277
  end
257
278
 
258
- def execute_event(event, thread, context: nil)
279
+ def execute_event(event, thread, adapter_name:, context: nil)
280
+ if %i[mention subscribed_message direct_message].include?(event.type)
281
+ route_message_event(event, thread, adapter_name, context)
282
+ return
283
+ end
284
+
259
285
  @registry.handlers_for(event).each do |handler|
260
- execute_handler(handler, event, thread, context: context)
286
+ execute_handler(handler, event, thread, adapter_name: adapter_name, context: context)
261
287
  end
262
288
  end
263
289
 
264
- def execute_handler(handler, event, thread, context: nil)
290
+ def route_message_event(event, thread, adapter_name, context)
291
+ message = event.message
292
+ dm_handlers = @registry.handlers_for_type(:direct_message, event: event)
293
+ if event.type == :direct_message && dm_handlers.any?
294
+ run_handlers(dm_handlers, event, thread, adapter_name, context)
295
+ return
296
+ end
297
+
298
+ message.is_mention = true if event.type == :direct_message
299
+
300
+ if thread&.subscribed?
301
+ run_handlers(@registry.handlers_for_type(:subscribed_message, event: event), event, thread, adapter_name, context)
302
+ return
303
+ end
304
+
305
+ if message.is_mention.nil?
306
+ message.is_mention = if event.type == :mention
307
+ true
308
+ else
309
+ detect_mention(message.text)
310
+ end
311
+ end
312
+
313
+ if message.is_mention
314
+ run_handlers(@registry.handlers_for_type(:mention, event: event), event, thread, adapter_name, context)
315
+ return
316
+ end
317
+
318
+ @registry.handlers_for_type(:new_message, event: event).each do |handler|
319
+ execute_handler(handler, event, thread, adapter_name: adapter_name, context: context)
320
+ end
321
+ end
322
+
323
+ def run_handlers(handlers, event, thread, adapter_name, context)
324
+ handlers.each do |handler|
325
+ execute_handler(handler, event, thread, adapter_name: adapter_name, context: context)
326
+ end
327
+ end
328
+
329
+ def detect_mention(text)
330
+ masked_text = ChatSDK::Format::Mentions.mask_code_spans(text)
331
+ /(?<![[:word:]])@#{Regexp.escape(@config.user_name)}\b/i.match?(masked_text)
332
+ end
333
+
334
+ def execute_handler(handler, event, thread, adapter_name:, context: nil)
265
335
  ChatSDK::Instrumentation.instrument("handler.chat_sdk", handler_type: event.type) do
266
336
  ChatSDK::AI::ConversationScope.with(thread) do
267
337
  case event.type
268
- when :mention, :subscribed_message, :direct_message
338
+ when :mention, :subscribed_message, :direct_message, :new_message
269
339
  handler.block.call(thread, event.message, context)
270
340
  when :message_updated
271
341
  handler.block.call(thread, event.message, event.previous_message)
272
- when :reaction, :action, :slash_command, :message_deleted
342
+ when :reaction, :action, :slash_command, :message_deleted, :installed, :uninstalled
273
343
  add_thread_to_event(event, thread)
274
344
  handler.block.call(event)
345
+ when *LIFECYCLE_EVENT_TYPES
346
+ handler.block.call(event)
275
347
  end
276
348
  end
277
349
  end
278
350
  rescue => e
279
- ChatSDK::Log.error("Handler error (#{event.type}): #{e.message}")
280
- ChatSDK::Log.debug(e.backtrace&.first(5)&.join("\n"))
351
+ @error_reporter.report(
352
+ e,
353
+ "Handler error (#{event.type})",
354
+ source: :handler,
355
+ adapter: adapter_name,
356
+ event_type: event.type,
357
+ thread_id: event.respond_to?(:thread_id) ? event.thread_id : nil,
358
+ channel_id: event.respond_to?(:channel_id) ? event.channel_id : nil
359
+ )
281
360
  end
282
361
 
283
362
  def add_thread_to_event(event, thread)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ class ErrorReporter
5
+ def initialize(callback = nil)
6
+ @callback = callback
7
+ end
8
+
9
+ def report(error, message, **context)
10
+ context = context.compact
11
+ ChatSDK::Log.error(format_entry(error, message, context))
12
+ reporter = @callback || self.class.default_callback
13
+ reporter&.call(error, context)
14
+ rescue => e
15
+ ChatSDK::Log.error("on_error callback failed: #{e.class}: #{e.message}")
16
+ end
17
+
18
+ def self.default_callback
19
+ return unless defined?(::Rails) && ::Rails.respond_to?(:error)
20
+
21
+ rails_reporter = ::Rails.error
22
+ return unless rails_reporter.respond_to?(:report)
23
+
24
+ ->(error, context) { rails_reporter.report(error, handled: true, severity: :error, context: context) }
25
+ end
26
+
27
+ private
28
+
29
+ def format_entry(error, message, context)
30
+ details = context.empty? ? "" : " (#{context.map { |key, value| "#{key}=#{value}" }.join(" ")})"
31
+ entry = "#{message}#{details}: #{error.class}: #{error.message}"
32
+ backtrace = error.backtrace
33
+ backtrace ? "#{entry}\n#{backtrace.join("\n")}" : entry
34
+ end
35
+ end
36
+ end
@@ -15,6 +15,7 @@ module ChatSDK
15
15
 
16
16
  class LockConflictError < Error; end
17
17
  class SignatureVerificationError < Error; end
18
+ class HandlerExecutorError < Error; end
18
19
 
19
20
  class PlatformError < Error
20
21
  attr_reader :status, :body, :adapter_name
@@ -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
@@ -16,14 +16,35 @@ module ChatSDK
16
16
  data.merge!(
17
17
  "message_id" => event.message_id,
18
18
  "previous_message" => dump_message(event.previous_message),
19
- "deleted_at" => event.deleted_at&.iso8601
19
+ "deleted_at" => event.deleted_at&.iso8601(6)
20
20
  )
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
+ )
36
+ when :assistant_thread_started, :assistant_context_changed
37
+ data.merge!(
38
+ "thread_ts" => event.thread_ts,
39
+ "user_id" => event.user_id,
40
+ "context" => event.context.to_h.transform_keys(&:to_s)
41
+ )
42
+ when :app_home_opened
43
+ data["user_id"] = event.user_id
44
+ data["tab"] = event.tab
45
+ when :member_joined_channel
46
+ data["user_id"] = event.user_id
47
+ data["inviter_id"] = event.inviter_id
27
48
  end
28
49
  data
29
50
  end
@@ -55,10 +76,45 @@ module ChatSDK
55
76
  when :reaction
56
77
  Events::Reaction.new(emoji: data["emoji"], user_id: data["user_id"], message_id: data["message_id"], added: data["added"], **common)
57
78
  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)
79
+ 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
80
  when :slash_command
60
81
  common.delete(:thread_id)
61
82
  Events::SlashCommand.new(command: data["command"], text: data["text"], user_id: data["user_id"], trigger_id: data["trigger_id"], **common)
83
+ when :installed, :uninstalled
84
+ common.delete(:thread_id)
85
+ common.delete(:channel_id)
86
+ event_class = if type == :installed
87
+ Events::Installed
88
+ else
89
+ Events::Uninstalled
90
+ end
91
+ event_class.new(
92
+ installation_id: data["installation_id"],
93
+ team_id: data["team_id"],
94
+ tenant_id: data["tenant_id"],
95
+ conversation_id: data["conversation_id"],
96
+ action: data["action"],
97
+ user: load_author(data["user"]),
98
+ **common
99
+ )
100
+ when :assistant_thread_started, :assistant_context_changed
101
+ event_class = if type == :assistant_thread_started
102
+ Events::AssistantThreadStarted
103
+ else
104
+ Events::AssistantContextChanged
105
+ end
106
+ event_class.new(
107
+ thread_ts: data["thread_ts"],
108
+ user_id: data["user_id"],
109
+ context: data["context"] || {},
110
+ **common
111
+ )
112
+ when :app_home_opened
113
+ common.delete(:thread_id)
114
+ Events::AppHomeOpened.new(user_id: data["user_id"], tab: data["tab"], **common)
115
+ when :member_joined_channel
116
+ common.delete(:thread_id)
117
+ Events::MemberJoinedChannel.new(user_id: data["user_id"], inviter_id: data["inviter_id"], **common)
62
118
  else
63
119
  raise ArgumentError, "unsupported queued event type: #{type}"
64
120
  end
@@ -74,7 +130,7 @@ module ChatSDK
74
130
  "thread_id" => event.respond_to?(:thread_id) ? event.thread_id : nil,
75
131
  "channel_id" => event.respond_to?(:channel_id) ? event.channel_id : nil,
76
132
  "raw" => json_safe(event.raw),
77
- "timestamp" => event.timestamp&.iso8601
133
+ "timestamp" => event.timestamp&.iso8601(6)
78
134
  }
79
135
  end
80
136
 
@@ -93,7 +149,8 @@ module ChatSDK
93
149
  "reply_to" => dump_message(message.reply_to),
94
150
  "subject" => json_safe(message.subject),
95
151
  "raw" => json_safe(message.raw),
96
- "timestamp" => message.timestamp&.iso8601
152
+ "timestamp" => message.timestamp&.iso8601(6),
153
+ "is_mention" => message.is_mention
97
154
  }
98
155
  end
99
156
 
@@ -113,7 +170,8 @@ module ChatSDK
113
170
  reply_to: load_message(data["reply_to"]),
114
171
  subject: data["subject"],
115
172
  raw: data["raw"],
116
- timestamp: parse_time(data["timestamp"])
173
+ timestamp: parse_time(data["timestamp"]),
174
+ is_mention: data["is_mention"]
117
175
  )
118
176
  end
119
177
 
@@ -125,6 +183,7 @@ module ChatSDK
125
183
  "name" => author.name,
126
184
  "platform" => author.platform.to_s,
127
185
  "bot" => author.bot?,
186
+ "me" => author.me?,
128
187
  "system" => author.system?,
129
188
  "locale" => author.locale,
130
189
  "email" => author.email,
@@ -141,6 +200,7 @@ module ChatSDK
141
200
  name: data["name"],
142
201
  platform: data["platform"].to_sym,
143
202
  bot: data["bot"],
203
+ me: data["me"],
144
204
  system: data["system"],
145
205
  locale: data["locale"],
146
206
  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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Events
5
+ class AppHomeOpened < Base
6
+ attr_reader :user_id, :channel_id, :tab
7
+
8
+ def initialize(user_id:, channel_id:, tab: nil, **kwargs)
9
+ super(type: :app_home_opened, **kwargs)
10
+ @user_id = user_id
11
+ @channel_id = channel_id
12
+ @tab = tab
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Events
5
+ # rubocop:disable Style/RedundantStructKeywordInit
6
+ AssistantContext = Struct.new(
7
+ :channel_id,
8
+ :team_id,
9
+ :enterprise_id,
10
+ :thread_entry_point,
11
+ :force_search,
12
+ keyword_init: true
13
+ )
14
+ # rubocop:enable Style/RedundantStructKeywordInit
15
+
16
+ def AssistantContext.coerce(value)
17
+ return value if value.is_a?(self)
18
+
19
+ value = {} if value.nil?
20
+ return value unless value.is_a?(Hash)
21
+
22
+ new(**value.transform_keys(&:to_sym).slice(*members))
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Events
5
+ class AssistantContextChanged < Base
6
+ attr_reader :thread_id, :channel_id, :thread_ts, :user_id, :context
7
+
8
+ def initialize(thread_id:, channel_id:, thread_ts:, user_id:, context: {}, **kwargs)
9
+ super(type: :assistant_context_changed, **kwargs)
10
+ @thread_id = thread_id
11
+ @channel_id = channel_id
12
+ @thread_ts = thread_ts
13
+ @user_id = user_id
14
+ @context = AssistantContext.coerce(context)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Events
5
+ class AssistantThreadStarted < Base
6
+ attr_reader :thread_id, :channel_id, :thread_ts, :user_id, :context
7
+
8
+ def initialize(thread_id:, channel_id:, thread_ts:, user_id:, context: {}, **kwargs)
9
+ super(type: :assistant_thread_started, **kwargs)
10
+ @thread_id = thread_id
11
+ @channel_id = channel_id
12
+ @thread_ts = thread_ts
13
+ @user_id = user_id
14
+ @context = AssistantContext.coerce(context)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Events
5
+ class MemberJoinedChannel < Base
6
+ attr_reader :user_id, :channel_id, :inviter_id
7
+
8
+ def initialize(user_id:, channel_id:, inviter_id: nil, **kwargs)
9
+ super(type: :member_joined_channel, **kwargs)
10
+ @user_id = user_id
11
+ @channel_id = channel_id
12
+ @inviter_id = inviter_id
13
+ end
14
+ end
15
+ end
16
+ 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