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.
- checksums.yaml +4 -4
- data/lib/chat_sdk/adapter/base.rb +19 -5
- data/lib/chat_sdk/adapter/invocation.rb +18 -0
- data/lib/chat_sdk/ai/conversation_scope.rb +11 -1
- data/lib/chat_sdk/ai/stream_handler.rb +3 -3
- data/lib/chat_sdk/ai/tool_builder.rb +2 -1
- data/lib/chat_sdk/ai/tool_executor.rb +32 -15
- data/lib/chat_sdk/author.rb +6 -1
- data/lib/chat_sdk/cards/actions_context.rb +26 -4
- data/lib/chat_sdk/cards/builder.rb +30 -4
- data/lib/chat_sdk/cards/node.rb +4 -0
- data/lib/chat_sdk/cards/renderer.rb +3 -0
- data/lib/chat_sdk/chat.rb +80 -5
- data/lib/chat_sdk/config.rb +34 -2
- data/lib/chat_sdk/dispatcher.rb +107 -28
- data/lib/chat_sdk/error_reporter.rb +36 -0
- data/lib/chat_sdk/errors.rb +1 -0
- data/lib/chat_sdk/event_registry.rb +7 -2
- data/lib/chat_sdk/event_serializer.rb +66 -6
- data/lib/chat_sdk/events/action.rb +3 -2
- data/lib/chat_sdk/events/app_home_opened.rb +16 -0
- data/lib/chat_sdk/events/assistant_context.rb +25 -0
- data/lib/chat_sdk/events/assistant_context_changed.rb +18 -0
- data/lib/chat_sdk/events/assistant_thread_started.rb +18 -0
- data/lib/chat_sdk/events/installed.rb +19 -0
- data/lib/chat_sdk/events/member_joined_channel.rb +16 -0
- data/lib/chat_sdk/events/uninstalled.rb +19 -0
- data/lib/chat_sdk/format/mentions.rb +61 -0
- data/lib/chat_sdk/message.rb +4 -1
- data/lib/chat_sdk/modals/builder.rb +39 -4
- data/lib/chat_sdk/postable_message.rb +3 -2
- data/lib/chat_sdk/streaming/chunk.rb +35 -0
- data/lib/chat_sdk/streaming/markdown_text_chunk.rb +15 -0
- data/lib/chat_sdk/streaming/plan_update_chunk.rb +15 -0
- data/lib/chat_sdk/streaming/stream.rb +48 -8
- data/lib/chat_sdk/streaming/task_update_chunk.rb +26 -0
- data/lib/chat_sdk/streaming/writer.rb +16 -0
- data/lib/chat_sdk/streaming_plan.rb +32 -0
- data/lib/chat_sdk/testing/fake_adapter.rb +6 -6
- data/lib/chat_sdk/thread.rb +86 -15
- data/lib/chat_sdk/version.rb +1 -1
- data/lib/chat_sdk/webhook/endpoint.rb +45 -2
- data/lib/chat_sdk.rb +2 -2
- metadata +17 -1
data/lib/chat_sdk/dispatcher.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
280
|
-
|
|
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
|
data/lib/chat_sdk/errors.rb
CHANGED
|
@@ -13,13 +13,18 @@ module ChatSDK
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def handlers_for(event)
|
|
16
|
-
|
|
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
|