Class: ChatSDK::Slack::Adapter
- Inherits:
-
Adapter::Base
- Object
- Adapter::Base
- ChatSDK::Slack::Adapter
- Defined in:
- lib/chat_sdk/slack/adapter.rb
Instance Method Summary collapse
- #ack_response(rack_request) ⇒ Object
- #add_reaction(channel_id:, message_id:, emoji:) ⇒ Object
-
#client ⇒ Object
Returns the per-request client (multi-workspace) or the static client (single-workspace).
- #delete_installation(team_id) ⇒ Object
- #delete_message(channel_id:, message_id:) ⇒ Object
- #edit_message(channel_id:, message_id:, message:) ⇒ Object
- #fetch_messages(channel_id:, thread_id: nil, cursor: nil, limit: 50) ⇒ Object
- #fetch_thread(channel_id:, thread_id: nil) ⇒ Object
- #get_installation(team_id) ⇒ Object
- #get_user(user_id) ⇒ Object
- #handle_oauth_callback(code:, redirect_uri: nil) ⇒ Object
-
#initialize(bot_token: nil, signing_secret: nil, client_id: nil, client_secret: nil) ⇒ Adapter
constructor
A new instance of Adapter.
- #mention(user_id) ⇒ Object
- #name ⇒ Object
- #open_dm(user_id) ⇒ Object
- #open_modal(trigger_id:, modal:) ⇒ Object
- #parse_events(rack_request) ⇒ Object
- #post_ephemeral(channel_id:, user_id:, message:, thread_id: nil) ⇒ Object
-
#post_message(channel_id:, message:, thread_id: nil) ⇒ Object
Outbound.
- #publish_home_view(user_id:, view:) ⇒ Object
- #remove_reaction(channel_id:, message_id:, emoji:) ⇒ Object
- #render(postable_message) ⇒ Object
- #schedule_message(channel_id:, message:, post_at:, thread_id: nil) ⇒ Object
- #send_to_response_url(response_url:, message:) ⇒ Object
- #set_assistant_status(channel_id:, thread_id:, status:) ⇒ Object
- #set_assistant_title(channel_id:, thread_id:, title:) ⇒ Object
-
#set_installation(team_id, bot_token:, bot_user_id: nil, team_name: nil) ⇒ Object
--- Multi-workspace installation management ---.
-
#set_state(state) ⇒ Object
Inject state store after initialization (e.g., from Chat instance).
- #set_suggested_prompts(channel_id:, thread_id:, prompts:) ⇒ Object
-
#start_socket_mode(app_token: nil, &block) ⇒ Object
Slack-specific: receive real-time events via Socket Mode WebSocket.
- #start_typing(channel_id:, thread_id: nil) ⇒ Object
- #stop_socket_mode ⇒ Object
- #update_modal(view_id:, modal:) ⇒ Object
- #upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil) ⇒ Object
-
#verify_request!(rack_request) ⇒ Object
Inbound.
Constructor Details
#initialize(bot_token: nil, signing_secret: nil, client_id: nil, client_secret: nil) ⇒ Adapter
Returns a new instance of Adapter.
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/chat_sdk/slack/adapter.rb', line 11 def initialize(bot_token: nil, signing_secret: nil, client_id: nil, client_secret: nil) @bot_token = bot_token || ENV["SLACK_BOT_TOKEN"] @signing_secret = signing_secret || ENV["SLACK_SIGNING_SECRET"] @client_id = client_id || ENV["SLACK_CLIENT_ID"] @client_secret = client_secret || ENV["SLACK_CLIENT_SECRET"] unless @bot_token || @client_id raise ChatSDK::ConfigurationError, "Slack bot_token or client_id required" end raise ChatSDK::ConfigurationError, "Slack signing_secret required" unless @signing_secret if @bot_token ::Slack.configure do |config| config.token = @bot_token end @client = ::Slack::Web::Client.new(token: @bot_token) end @renderer = BlockKitRenderer.new @modal_renderer = ModalRenderer.new @team_clients = {} end |
Instance Method Details
#ack_response(rack_request) ⇒ Object
128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/chat_sdk/slack/adapter.rb', line 128 def ack_response(rack_request) body = rack_request.body.read rack_request.body.rewind parsed = parse_body(body, rack_request.content_type) return nil unless parsed if parsed["type"] == "url_verification" [200, {"content-type" => "text/plain"}, [parsed["challenge"]]] end end |
#add_reaction(channel_id:, message_id:, emoji:) ⇒ Object
209 210 211 |
# File 'lib/chat_sdk/slack/adapter.rb', line 209 def add_reaction(channel_id:, message_id:, emoji:) client.reactions_add(channel: channel_id, timestamp: , name: emoji) end |
#client ⇒ Object
Returns the per-request client (multi-workspace) or the static client (single-workspace).
36 37 38 |
# File 'lib/chat_sdk/slack/adapter.rb', line 36 def client ::Thread.current[:chat_sdk_slack_client] || @client end |
#delete_installation(team_id) ⇒ Object
64 65 66 67 68 69 |
# File 'lib/chat_sdk/slack/adapter.rb', line 64 def delete_installation(team_id) @team_clients.delete(team_id) return unless @state @state.delete(installation_key(team_id)) end |
#delete_message(channel_id:, message_id:) ⇒ Object
188 189 190 |
# File 'lib/chat_sdk/slack/adapter.rb', line 188 def (channel_id:, message_id:) client.chat_delete(channel: channel_id, ts: ) end |
#edit_message(channel_id:, message_id:, message:) ⇒ Object
179 180 181 182 183 184 185 186 |
# File 'lib/chat_sdk/slack/adapter.rb', line 179 def (channel_id:, message_id:, message:) msg = ChatSDK::PostableMessage.from() params = {channel: channel_id, ts: } (params, msg) client.chat_update(**params) end |
#fetch_messages(channel_id:, thread_id: nil, cursor: nil, limit: 50) ⇒ Object
255 256 257 258 259 260 261 262 263 |
# File 'lib/chat_sdk/slack/adapter.rb', line 255 def (channel_id:, thread_id: nil, cursor: nil, limit: 50) result = if thread_id client.conversations_replies(channel: channel_id, ts: thread_id, cursor: cursor, limit: limit) else client.conversations_history(channel: channel_id, cursor: cursor, limit: limit) end = result["messages"].map { |m| (m, channel_id) } [, result["response_metadata"]&.dig("next_cursor")] end |
#fetch_thread(channel_id:, thread_id: nil) ⇒ Object
312 313 314 315 |
# File 'lib/chat_sdk/slack/adapter.rb', line 312 def fetch_thread(channel_id:, thread_id: nil) result = client.conversations_info(channel: channel_id) result["channel"] end |
#get_installation(team_id) ⇒ Object
58 59 60 61 62 |
# File 'lib/chat_sdk/slack/adapter.rb', line 58 def get_installation(team_id) return nil unless @state @state.get(installation_key(team_id)) end |
#get_user(user_id) ⇒ Object
217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/chat_sdk/slack/adapter.rb', line 217 def get_user(user_id) result = client.users_info(user: user_id) return nil unless result&.dig("user", "id") ChatSDK::Author.new( id: result.dig("user", "id"), name: result.dig("user", "name"), platform: :slack, bot: result.dig("user", "is_bot") || false, raw: result ) end |
#handle_oauth_callback(code:, redirect_uri: nil) ⇒ Object
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/chat_sdk/slack/adapter.rb', line 71 def handle_oauth_callback(code:, redirect_uri: nil) raise ChatSDK::ConfigurationError, "client_id required for OAuth" unless @client_id temp_client = ::Slack::Web::Client.new params = { client_id: @client_id, client_secret: @client_secret, code: code } params[:redirect_uri] = redirect_uri if redirect_uri result = temp_client.oauth_v2_access(**params) team_id = result["team"]["id"] installation = { "bot_token" => result["access_token"], "bot_user_id" => result["bot_user_id"], "team_name" => result["team"]["name"] } set_installation(team_id, bot_token: installation["bot_token"], bot_user_id: installation["bot_user_id"], team_name: installation["team_name"]) {team_id: team_id, installation: installation} end |
#mention(user_id) ⇒ Object
342 343 344 |
# File 'lib/chat_sdk/slack/adapter.rb', line 342 def mention(user_id) "<@#{user_id}>" end |
#name ⇒ Object
99 100 101 |
# File 'lib/chat_sdk/slack/adapter.rb', line 99 def name :slack end |
#open_dm(user_id) ⇒ Object
230 231 232 233 |
# File 'lib/chat_sdk/slack/adapter.rb', line 230 def open_dm(user_id) result = client.conversations_open(users: user_id) result["channel"]["id"] end |
#open_modal(trigger_id:, modal:) ⇒ Object
265 266 267 268 |
# File 'lib/chat_sdk/slack/adapter.rb', line 265 def open_modal(trigger_id:, modal:) view = @modal_renderer.render(modal) client.views_open(trigger_id: trigger_id, view: view) end |
#parse_events(rack_request) ⇒ Object
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/chat_sdk/slack/adapter.rb', line 140 def parse_events(rack_request) body = rack_request.body.read rack_request.body.rewind parsed = parse_body(body, rack_request.content_type) return [] unless parsed # Multi-workspace: resolve per-team client from payload # Clear any stale client from a previous request on this thread (Puma thread pool safety) if @client_id ::Thread.current[:chat_sdk_slack_client] = nil team_id = parsed["team_id"] || parsed.dig("team", "id") resolve_team_client(team_id) if team_id end EventParser.parse(parsed) end |
#post_ephemeral(channel_id:, user_id:, message:, thread_id: nil) ⇒ Object
192 193 194 195 196 197 198 199 200 |
# File 'lib/chat_sdk/slack/adapter.rb', line 192 def post_ephemeral(channel_id:, user_id:, message:, thread_id: nil) msg = ChatSDK::PostableMessage.from() params = {channel: channel_id, user: user_id} params[:thread_ts] = thread_id if thread_id (params, msg) client.chat_postEphemeral(**params) end |
#post_message(channel_id:, message:, thread_id: nil) ⇒ Object
Outbound
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/chat_sdk/slack/adapter.rb', line 159 def (channel_id:, message:, thread_id: nil) msg = ChatSDK::PostableMessage.from() params = {channel: channel_id} params[:thread_ts] = thread_id if thread_id (params, msg) result = client.chat_postMessage(**params) ChatSDK::Message.new( id: result["ts"], text: msg.text || "", author: ChatSDK::Author.new(id: "bot", name: "bot", platform: :slack, bot: true), thread_id: thread_id || result["ts"], channel_id: channel_id, platform: :slack, raw: result ) end |
#publish_home_view(user_id:, view:) ⇒ Object
274 275 276 |
# File 'lib/chat_sdk/slack/adapter.rb', line 274 def publish_home_view(user_id:, view:) client.views_publish(user_id: user_id, view: view) end |
#remove_reaction(channel_id:, message_id:, emoji:) ⇒ Object
213 214 215 |
# File 'lib/chat_sdk/slack/adapter.rb', line 213 def remove_reaction(channel_id:, message_id:, emoji:) client.reactions_remove(channel: channel_id, timestamp: , name: emoji) end |
#render(postable_message) ⇒ Object
346 347 348 349 350 351 352 |
# File 'lib/chat_sdk/slack/adapter.rb', line 346 def render() if .card? @renderer.render(.card) else .text end end |
#schedule_message(channel_id:, message:, post_at:, thread_id: nil) ⇒ Object
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/chat_sdk/slack/adapter.rb', line 235 def (channel_id:, message:, post_at:, thread_id: nil) msg = ChatSDK::PostableMessage.from() text = msg.text || "" params = {channel: channel_id, text: text, post_at: post_at.to_i} params[:thread_ts] = thread_id if thread_id result = client.chat_scheduleMessage(**params) ChatSDK::Message.new( id: result["scheduled_message_id"], text: text, author: ChatSDK::Author.new(id: "bot", name: "bot", platform: :slack, bot: true), thread_id: thread_id || result["scheduled_message_id"], channel_id: channel_id, platform: :slack, raw: result ) end |
#send_to_response_url(response_url:, message:) ⇒ Object
302 303 304 305 306 307 308 309 310 |
# File 'lib/chat_sdk/slack/adapter.rb', line 302 def send_to_response_url(response_url:, message:) msg = ChatSDK::PostableMessage.from() payload = {} (payload, msg) Faraday.post(response_url) do |req| req.headers["Content-Type"] = "application/json" req.body = JSON.generate(payload) end end |
#set_assistant_status(channel_id:, thread_id:, status:) ⇒ Object
286 287 288 289 290 291 292 |
# File 'lib/chat_sdk/slack/adapter.rb', line 286 def set_assistant_status(channel_id:, thread_id:, status:) client.assistant_threads_setStatus( channel_id: channel_id, thread_ts: thread_id, status: status ) end |
#set_assistant_title(channel_id:, thread_id:, title:) ⇒ Object
294 295 296 297 298 299 300 |
# File 'lib/chat_sdk/slack/adapter.rb', line 294 def set_assistant_title(channel_id:, thread_id:, title:) client.assistant_threads_setTitle( channel_id: channel_id, thread_ts: thread_id, title: title ) end |
#set_installation(team_id, bot_token:, bot_user_id: nil, team_name: nil) ⇒ Object
--- Multi-workspace installation management ---
47 48 49 50 51 52 53 54 55 56 |
# File 'lib/chat_sdk/slack/adapter.rb', line 47 def set_installation(team_id, bot_token:, bot_user_id: nil, team_name: nil) raise ChatSDK::ConfigurationError, "Multi-workspace mode requires state" unless @state @team_clients.delete(team_id) @state.set(installation_key(team_id), { "bot_token" => bot_token, "bot_user_id" => bot_user_id, "team_name" => team_name }) end |
#set_state(state) ⇒ Object
Inject state store after initialization (e.g., from Chat instance).
41 42 43 |
# File 'lib/chat_sdk/slack/adapter.rb', line 41 def set_state(state) @state = state end |
#set_suggested_prompts(channel_id:, thread_id:, prompts:) ⇒ Object
278 279 280 281 282 283 284 |
# File 'lib/chat_sdk/slack/adapter.rb', line 278 def set_suggested_prompts(channel_id:, thread_id:, prompts:) client.assistant_threads_setSuggestedPrompts( channel_id: channel_id, thread_ts: thread_id, prompts: prompts ) end |
#start_socket_mode(app_token: nil, &block) ⇒ Object
Slack-specific: receive real-time events via Socket Mode WebSocket. Requires the optional 'faye-websocket' gem and an app-level token (xapp-*). Not part of the base adapter contract.
Usage:
adapter.start_socket_mode(app_token: "xapp-...") do |event|
# event is a ChatSDK::Events::* instance
end
330 331 332 333 334 335 336 |
# File 'lib/chat_sdk/slack/adapter.rb', line 330 def start_socket_mode(app_token: nil, &block) app_token ||= ENV["SLACK_APP_TOKEN"] raise ChatSDK::ConfigurationError, "Slack app_token required for socket mode" unless app_token @socket_mode = SocketMode.new(app_token: app_token, bot_client: client) @socket_mode.start(&block) end |
#start_typing(channel_id:, thread_id: nil) ⇒ Object
317 318 319 320 |
# File 'lib/chat_sdk/slack/adapter.rb', line 317 def start_typing(channel_id:, thread_id: nil) # Slack doesn't have a native typing indicator API for bots # This is a no-op but the capability is declared for streaming support end |
#stop_socket_mode ⇒ Object
338 339 340 |
# File 'lib/chat_sdk/slack/adapter.rb', line 338 def stop_socket_mode @socket_mode&.stop end |
#update_modal(view_id:, modal:) ⇒ Object
270 271 272 |
# File 'lib/chat_sdk/slack/adapter.rb', line 270 def update_modal(view_id:, modal:) client.views_update(view_id: view_id, view: @modal_renderer.render(modal)) end |
#upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil) ⇒ Object
202 203 204 205 206 207 |
# File 'lib/chat_sdk/slack/adapter.rb', line 202 def upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil) params = {channels: channel_id, file: Faraday::Multipart::FilePart.new(io, nil, filename)} params[:thread_ts] = thread_id if thread_id params[:initial_comment] = comment if comment client.files_upload(**params) end |
#verify_request!(rack_request) ⇒ Object
Inbound
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/chat_sdk/slack/adapter.rb', line 104 def verify_request!(rack_request) body = rack_request.body.read rack_request.body.rewind = rack_request.env["HTTP_X_SLACK_REQUEST_TIMESTAMP"] signature = rack_request.env["HTTP_X_SLACK_SIGNATURE"] raise ChatSDK::SignatureVerificationError, "Missing Slack signature headers" unless && signature sig_basestring = "v0:#{}:#{body}" hex_digest = OpenSSL::HMAC.hexdigest("SHA256", @signing_secret, sig_basestring) computed = "v0=#{hex_digest}" unless Rack::Utils.secure_compare(computed, signature) raise ChatSDK::SignatureVerificationError, "Invalid Slack signature" end age = Time.now.to_i - .to_i if age.abs > 300 raise ChatSDK::SignatureVerificationError, "Slack request too old (#{age}s)" end true end |