chat_sdk-slack 0.9.0 → 1.0.1

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 (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/chat_sdk/slack/adapter.rb +148 -26
  3. metadata +3 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c350492bf0bf3f0398f8146794cfd21c57218ac69c0eaa2fca06123e1ebd91cd
4
- data.tar.gz: a538ab5f51da90868d3fd4b018ba33d98b97d703d952844909cdc614e5a08e10
3
+ metadata.gz: f3267f830fd79846f929e7e61be15c2b65d1e6183f6f5d56271f88fbb6fdf85b
4
+ data.tar.gz: 9fe0cd67184ef4561a2e6ccb6d5678f5a639f2b53add2be5ef6436b239a1a7c3
5
5
  SHA512:
6
- metadata.gz: e5b9e7c785c898a7826562ce527b3a9dc5753adf42459f6248e3e0f363e5ec5478af2a959f8b2d30978ff8e7234c63838d8ee96a68172ac20c647f512073d61a
7
- data.tar.gz: 0b78d5335690d409deedc6a69f45a57093ab559a35a0e20f1ee4518b883afc924f5919a4b2f46e1b50dc12fd443ae7eb9547c3c268daff1928c6db37244f1902
6
+ metadata.gz: c686e9ac2a237c82166200783289f2310cb57cc3760a4d55b11447fa0f7a746b10ffb40b713d18334f277f3efb91663733977f082cf0152ee96ed387ddbd2ed1
7
+ data.tar.gz: 1e897869f36b3571e31dea33be0e78f9bd46bcbc5e3c839da11453337242378933dcfec8eec289ceb4c6cbae4c339bce0a1c4399f39fe74a7980703f5e1e4c40
@@ -8,22 +8,92 @@ module ChatSDK
8
8
  :streaming_edit, :threads, :direct_messages, :message_history,
9
9
  :scheduled_messages
10
10
 
11
- attr_reader :client
12
-
13
- def initialize(bot_token: nil, signing_secret: nil)
11
+ def initialize(bot_token: nil, signing_secret: nil,
12
+ client_id: nil, client_secret: nil)
14
13
  @bot_token = bot_token || ENV["SLACK_BOT_TOKEN"]
15
14
  @signing_secret = signing_secret || ENV["SLACK_SIGNING_SECRET"]
15
+ @client_id = client_id || ENV["SLACK_CLIENT_ID"]
16
+ @client_secret = client_secret || ENV["SLACK_CLIENT_SECRET"]
16
17
 
17
- raise ChatSDK::ConfigurationError, "Slack bot_token required" unless @bot_token
18
+ unless @bot_token || @client_id
19
+ raise ChatSDK::ConfigurationError, "Slack bot_token or client_id required"
20
+ end
18
21
  raise ChatSDK::ConfigurationError, "Slack signing_secret required" unless @signing_secret
19
22
 
20
- ::Slack.configure do |config|
21
- config.token = @bot_token
23
+ if @bot_token
24
+ ::Slack.configure do |config|
25
+ config.token = @bot_token
26
+ end
27
+ @client = ::Slack::Web::Client.new(token: @bot_token)
22
28
  end
23
29
 
24
- @client = ::Slack::Web::Client.new(token: @bot_token)
25
30
  @renderer = BlockKitRenderer.new
26
31
  @modal_renderer = ModalRenderer.new
32
+ @team_clients = {}
33
+ end
34
+
35
+ # Returns the per-request client (multi-workspace) or the static client (single-workspace).
36
+ def client
37
+ ::Thread.current[:chat_sdk_slack_client] || @client
38
+ end
39
+
40
+ # Inject state store after initialization (e.g., from Chat instance).
41
+ def set_state(state)
42
+ @state = state
43
+ end
44
+
45
+ # --- Multi-workspace installation management ---
46
+
47
+ def set_installation(team_id, bot_token:, bot_user_id: nil, team_name: nil)
48
+ raise ChatSDK::ConfigurationError, "Multi-workspace mode requires state" unless @state
49
+
50
+ @team_clients.delete(team_id)
51
+ @state.set(installation_key(team_id), {
52
+ "bot_token" => bot_token,
53
+ "bot_user_id" => bot_user_id,
54
+ "team_name" => team_name
55
+ })
56
+ end
57
+
58
+ def get_installation(team_id)
59
+ return nil unless @state
60
+
61
+ @state.get(installation_key(team_id))
62
+ end
63
+
64
+ def delete_installation(team_id)
65
+ @team_clients.delete(team_id)
66
+ return unless @state
67
+
68
+ @state.delete(installation_key(team_id))
69
+ end
70
+
71
+ def handle_oauth_callback(code:, redirect_uri: nil)
72
+ raise ChatSDK::ConfigurationError, "client_id required for OAuth" unless @client_id
73
+
74
+ temp_client = ::Slack::Web::Client.new
75
+ params = {
76
+ client_id: @client_id,
77
+ client_secret: @client_secret,
78
+ code: code
79
+ }
80
+ params[:redirect_uri] = redirect_uri if redirect_uri
81
+
82
+ result = temp_client.oauth_v2_access(**params)
83
+
84
+ team_id = result["team"]["id"]
85
+ installation = {
86
+ "bot_token" => result["access_token"],
87
+ "bot_user_id" => result["bot_user_id"],
88
+ "team_name" => result["team"]["name"]
89
+ }
90
+
91
+ set_installation(team_id,
92
+ bot_token: installation["bot_token"],
93
+ bot_user_id: installation["bot_user_id"],
94
+ team_name: installation["team_name"])
95
+
96
+ {team_id: team_id, installation: installation}
27
97
  end
28
98
 
29
99
  def name
@@ -74,6 +144,14 @@ module ChatSDK
74
144
  parsed = parse_body(body, rack_request.content_type)
75
145
  return [] unless parsed
76
146
 
147
+ # Multi-workspace: resolve per-team client from payload
148
+ # Clear any stale client from a previous request on this thread (Puma thread pool safety)
149
+ if @client_id
150
+ ::Thread.current[:chat_sdk_slack_client] = nil
151
+ team_id = parsed["team_id"] || parsed.dig("team", "id")
152
+ resolve_team_client(team_id) if team_id
153
+ end
154
+
77
155
  EventParser.parse(parsed)
78
156
  end
79
157
 
@@ -85,7 +163,7 @@ module ChatSDK
85
163
 
86
164
  apply_message_params(params, msg)
87
165
 
88
- result = @client.chat_postMessage(**params)
166
+ result = client.chat_postMessage(**params)
89
167
 
90
168
  ChatSDK::Message.new(
91
169
  id: result["ts"],
@@ -104,11 +182,11 @@ module ChatSDK
104
182
 
105
183
  apply_message_params(params, msg)
106
184
 
107
- @client.chat_update(**params)
185
+ client.chat_update(**params)
108
186
  end
109
187
 
110
188
  def delete_message(channel_id:, message_id:)
111
- @client.chat_delete(channel: channel_id, ts: message_id)
189
+ client.chat_delete(channel: channel_id, ts: message_id)
112
190
  end
113
191
 
114
192
  def post_ephemeral(channel_id:, user_id:, message:, thread_id: nil)
@@ -118,26 +196,26 @@ module ChatSDK
118
196
 
119
197
  apply_message_params(params, msg)
120
198
 
121
- @client.chat_postEphemeral(**params)
199
+ client.chat_postEphemeral(**params)
122
200
  end
123
201
 
124
202
  def upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil)
125
203
  params = {channels: channel_id, file: Faraday::Multipart::FilePart.new(io, nil, filename)}
126
204
  params[:thread_ts] = thread_id if thread_id
127
205
  params[:initial_comment] = comment if comment
128
- @client.files_upload(**params)
206
+ client.files_upload(**params)
129
207
  end
130
208
 
131
209
  def add_reaction(channel_id:, message_id:, emoji:)
132
- @client.reactions_add(channel: channel_id, timestamp: message_id, name: emoji)
210
+ client.reactions_add(channel: channel_id, timestamp: message_id, name: emoji)
133
211
  end
134
212
 
135
213
  def remove_reaction(channel_id:, message_id:, emoji:)
136
- @client.reactions_remove(channel: channel_id, timestamp: message_id, name: emoji)
214
+ client.reactions_remove(channel: channel_id, timestamp: message_id, name: emoji)
137
215
  end
138
216
 
139
217
  def get_user(user_id)
140
- result = @client.users_info(user: user_id)
218
+ result = client.users_info(user: user_id)
141
219
  return nil unless result&.dig("user", "id")
142
220
 
143
221
  ChatSDK::Author.new(
@@ -150,7 +228,7 @@ module ChatSDK
150
228
  end
151
229
 
152
230
  def open_dm(user_id)
153
- result = @client.conversations_open(users: user_id)
231
+ result = client.conversations_open(users: user_id)
154
232
  result["channel"]["id"]
155
233
  end
156
234
 
@@ -161,7 +239,7 @@ module ChatSDK
161
239
  params = {channel: channel_id, text: text, post_at: post_at.to_i}
162
240
  params[:thread_ts] = thread_id if thread_id
163
241
 
164
- result = @client.chat_scheduleMessage(**params)
242
+ result = client.chat_scheduleMessage(**params)
165
243
 
166
244
  ChatSDK::Message.new(
167
245
  id: result["scheduled_message_id"],
@@ -176,9 +254,9 @@ module ChatSDK
176
254
 
177
255
  def fetch_messages(channel_id:, thread_id: nil, cursor: nil, limit: 50)
178
256
  result = if thread_id
179
- @client.conversations_replies(channel: channel_id, ts: thread_id, cursor: cursor, limit: limit)
257
+ client.conversations_replies(channel: channel_id, ts: thread_id, cursor: cursor, limit: limit)
180
258
  else
181
- @client.conversations_history(channel: channel_id, cursor: cursor, limit: limit)
259
+ client.conversations_history(channel: channel_id, cursor: cursor, limit: limit)
182
260
  end
183
261
  messages = result["messages"].map { |m| parse_slack_message(m, channel_id) }
184
262
  [messages, result["response_metadata"]&.dig("next_cursor")]
@@ -186,15 +264,19 @@ module ChatSDK
186
264
 
187
265
  def open_modal(trigger_id:, modal:)
188
266
  view = @modal_renderer.render(modal)
189
- @client.views_open(trigger_id: trigger_id, view: view)
267
+ client.views_open(trigger_id: trigger_id, view: view)
268
+ end
269
+
270
+ def update_modal(view_id:, modal:)
271
+ client.views_update(view_id: view_id, view: @modal_renderer.render(modal))
190
272
  end
191
273
 
192
274
  def publish_home_view(user_id:, view:)
193
- @client.views_publish(user_id: user_id, view: view)
275
+ client.views_publish(user_id: user_id, view: view)
194
276
  end
195
277
 
196
278
  def set_suggested_prompts(channel_id:, thread_id:, prompts:)
197
- @client.assistant_threads_setSuggestedPrompts(
279
+ client.assistant_threads_setSuggestedPrompts(
198
280
  channel_id: channel_id,
199
281
  thread_ts: thread_id,
200
282
  prompts: prompts
@@ -202,7 +284,7 @@ module ChatSDK
202
284
  end
203
285
 
204
286
  def set_assistant_status(channel_id:, thread_id:, status:)
205
- @client.assistant_threads_setStatus(
287
+ client.assistant_threads_setStatus(
206
288
  channel_id: channel_id,
207
289
  thread_ts: thread_id,
208
290
  status: status
@@ -210,13 +292,28 @@ module ChatSDK
210
292
  end
211
293
 
212
294
  def set_assistant_title(channel_id:, thread_id:, title:)
213
- @client.assistant_threads_setTitle(
295
+ client.assistant_threads_setTitle(
214
296
  channel_id: channel_id,
215
297
  thread_ts: thread_id,
216
298
  title: title
217
299
  )
218
300
  end
219
301
 
302
+ def send_to_response_url(response_url:, message:)
303
+ msg = ChatSDK::PostableMessage.from(message)
304
+ payload = {}
305
+ apply_message_params(payload, msg)
306
+ Faraday.post(response_url) do |req|
307
+ req.headers["Content-Type"] = "application/json"
308
+ req.body = JSON.generate(payload)
309
+ end
310
+ end
311
+
312
+ def fetch_thread(channel_id:, thread_id: nil)
313
+ result = client.conversations_info(channel: channel_id)
314
+ result["channel"]
315
+ end
316
+
220
317
  def start_typing(channel_id:, thread_id: nil)
221
318
  # Slack doesn't have a native typing indicator API for bots
222
319
  # This is a no-op but the capability is declared for streaming support
@@ -234,8 +331,12 @@ module ChatSDK
234
331
  app_token ||= ENV["SLACK_APP_TOKEN"]
235
332
  raise ChatSDK::ConfigurationError, "Slack app_token required for socket mode" unless app_token
236
333
 
237
- socket = SocketMode.new(app_token: app_token, bot_client: @client)
238
- socket.start(&block)
334
+ @socket_mode = SocketMode.new(app_token: app_token, bot_client: client)
335
+ @socket_mode.start(&block)
336
+ end
337
+
338
+ def stop_socket_mode
339
+ @socket_mode&.stop
239
340
  end
240
341
 
241
342
  def mention(user_id)
@@ -252,6 +353,27 @@ module ChatSDK
252
353
 
253
354
  private
254
355
 
356
+ def resolve_team_client(team_id)
357
+ return unless @client_id && @state
358
+
359
+ cached = @team_clients[team_id]
360
+ if cached
361
+ ::Thread.current[:chat_sdk_slack_client] = cached
362
+ return
363
+ end
364
+
365
+ installation = get_installation(team_id)
366
+ return unless installation
367
+
368
+ new_client = ::Slack::Web::Client.new(token: installation["bot_token"])
369
+ @team_clients[team_id] = new_client
370
+ ::Thread.current[:chat_sdk_slack_client] = new_client
371
+ end
372
+
373
+ def installation_key(team_id)
374
+ "slack:installation:#{team_id}"
375
+ end
376
+
255
377
  def apply_message_params(params, msg)
256
378
  if msg.card?
257
379
  params[:blocks] = @renderer.render(msg.card)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chat_sdk-slack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Quentin Rousseau
@@ -13,14 +13,14 @@ dependencies:
13
13
  name: chat_sdk
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: '0.1'
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
25
  version: '0.1'
26
26
  - !ruby/object:Gem::Dependency