chat_sdk-slack 0.1.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 +7 -0
- data/lib/chat_sdk/slack/adapter.rb +231 -0
- data/lib/chat_sdk/slack/block_kit_renderer.rb +124 -0
- data/lib/chat_sdk/slack/event_parser.rb +171 -0
- data/lib/chat_sdk/slack/modal_renderer.rb +73 -0
- data/lib/chat_sdk/slack.rb +24 -0
- metadata +74 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d6e4e9dd2243b38d51bcdd10278e9f59ac540e01f7340ece7585d63475c59e06
|
|
4
|
+
data.tar.gz: 2f7a0a47486dcfa63cd3be0c4784ba70f5d8f973beed2b276b4ce10aebb5a242
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c1d6bc38dc2635d2e737c915f2eb24c32c16f0c9ed7792af675769ddebbe3e14de8b679c28566ca9e5655b4332860a58424aa5bff6901eaa3120baf17051c80b
|
|
7
|
+
data.tar.gz: 9f3af6fb31887b727e0ff1b9bbfcabdb57e9dc1008b915f4b25ae13a7ef22c794ea5042a42d7117c31b990233dfdc69de819e642d55b581f57dfa2d3b7fb80f4
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Slack
|
|
5
|
+
class Adapter < ChatSDK::Adapter::Base
|
|
6
|
+
capabilities :edit_messages, :delete_messages, :ephemeral_messages,
|
|
7
|
+
:file_uploads, :reactions, :modals, :typing_indicator,
|
|
8
|
+
:streaming_edit, :threads, :direct_messages, :message_history
|
|
9
|
+
|
|
10
|
+
attr_reader :client
|
|
11
|
+
|
|
12
|
+
def initialize(bot_token: nil, signing_secret: nil)
|
|
13
|
+
@bot_token = bot_token || ENV["SLACK_BOT_TOKEN"]
|
|
14
|
+
@signing_secret = signing_secret || ENV["SLACK_SIGNING_SECRET"]
|
|
15
|
+
|
|
16
|
+
raise ChatSDK::ConfigurationError, "Slack bot_token required" unless @bot_token
|
|
17
|
+
raise ChatSDK::ConfigurationError, "Slack signing_secret required" unless @signing_secret
|
|
18
|
+
|
|
19
|
+
::Slack.configure do |config|
|
|
20
|
+
config.token = @bot_token
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@client = ::Slack::Web::Client.new(token: @bot_token)
|
|
24
|
+
@renderer = BlockKitRenderer.new
|
|
25
|
+
@modal_renderer = ModalRenderer.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def name
|
|
29
|
+
:slack
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Inbound
|
|
33
|
+
def verify_request!(rack_request)
|
|
34
|
+
body = rack_request.body.read
|
|
35
|
+
rack_request.body.rewind
|
|
36
|
+
timestamp = rack_request.env["HTTP_X_SLACK_REQUEST_TIMESTAMP"]
|
|
37
|
+
signature = rack_request.env["HTTP_X_SLACK_SIGNATURE"]
|
|
38
|
+
|
|
39
|
+
raise ChatSDK::SignatureVerificationError, "Missing Slack signature headers" unless timestamp && signature
|
|
40
|
+
|
|
41
|
+
sig_basestring = "v0:#{timestamp}:#{body}"
|
|
42
|
+
hex_digest = OpenSSL::HMAC.hexdigest("SHA256", @signing_secret, sig_basestring)
|
|
43
|
+
computed = "v0=#{hex_digest}"
|
|
44
|
+
|
|
45
|
+
unless Rack::Utils.secure_compare(computed, signature)
|
|
46
|
+
raise ChatSDK::SignatureVerificationError, "Invalid Slack signature"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
age = Time.now.to_i - timestamp.to_i
|
|
50
|
+
if age.abs > 300
|
|
51
|
+
raise ChatSDK::SignatureVerificationError, "Slack request too old (#{age}s)"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ack_response(rack_request)
|
|
58
|
+
body = rack_request.body.read
|
|
59
|
+
rack_request.body.rewind
|
|
60
|
+
|
|
61
|
+
parsed = parse_body(body, rack_request.content_type)
|
|
62
|
+
return nil unless parsed
|
|
63
|
+
|
|
64
|
+
if parsed["type"] == "url_verification"
|
|
65
|
+
[200, {"content-type" => "text/plain"}, [parsed["challenge"]]]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_events(rack_request)
|
|
70
|
+
body = rack_request.body.read
|
|
71
|
+
rack_request.body.rewind
|
|
72
|
+
|
|
73
|
+
parsed = parse_body(body, rack_request.content_type)
|
|
74
|
+
return [] unless parsed
|
|
75
|
+
|
|
76
|
+
EventParser.parse(parsed)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Outbound
|
|
80
|
+
def post_message(channel_id:, message:, thread_id: nil)
|
|
81
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
82
|
+
params = {channel: channel_id}
|
|
83
|
+
params[:thread_ts] = thread_id if thread_id
|
|
84
|
+
|
|
85
|
+
if msg.card?
|
|
86
|
+
rendered = @renderer.render(msg.card)
|
|
87
|
+
params[:blocks] = rendered
|
|
88
|
+
params[:text] = msg.text || msg.card.fallback_text
|
|
89
|
+
else
|
|
90
|
+
params[:text] = msg.text
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
result = @client.chat_postMessage(**params)
|
|
94
|
+
|
|
95
|
+
ChatSDK::Message.new(
|
|
96
|
+
id: result["ts"],
|
|
97
|
+
text: msg.text || "",
|
|
98
|
+
author: ChatSDK::Author.new(id: "bot", name: "bot", platform: :slack, bot: true),
|
|
99
|
+
thread_id: thread_id || result["ts"],
|
|
100
|
+
channel_id: channel_id,
|
|
101
|
+
platform: :slack,
|
|
102
|
+
raw: result
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def edit_message(channel_id:, message_id:, message:)
|
|
107
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
108
|
+
params = {channel: channel_id, ts: message_id}
|
|
109
|
+
|
|
110
|
+
if msg.card?
|
|
111
|
+
params[:blocks] = @renderer.render(msg.card)
|
|
112
|
+
params[:text] = msg.text || msg.card.fallback_text
|
|
113
|
+
else
|
|
114
|
+
params[:text] = msg.text
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@client.chat_update(**params)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def delete_message(channel_id:, message_id:)
|
|
121
|
+
@client.chat_delete(channel: channel_id, ts: message_id)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def post_ephemeral(channel_id:, user_id:, message:, thread_id: nil)
|
|
125
|
+
msg = ChatSDK::PostableMessage.from(message)
|
|
126
|
+
params = {channel: channel_id, user: user_id}
|
|
127
|
+
params[:thread_ts] = thread_id if thread_id
|
|
128
|
+
|
|
129
|
+
if msg.card?
|
|
130
|
+
params[:blocks] = @renderer.render(msg.card)
|
|
131
|
+
params[:text] = msg.text || msg.card.fallback_text
|
|
132
|
+
else
|
|
133
|
+
params[:text] = msg.text
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
@client.chat_postEphemeral(**params)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil)
|
|
140
|
+
params = {channels: channel_id, file: Faraday::Multipart::FilePart.new(io, nil, filename)}
|
|
141
|
+
params[:thread_ts] = thread_id if thread_id
|
|
142
|
+
params[:initial_comment] = comment if comment
|
|
143
|
+
@client.files_upload(**params)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def add_reaction(channel_id:, message_id:, emoji:)
|
|
147
|
+
@client.reactions_add(channel: channel_id, timestamp: message_id, name: emoji)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def remove_reaction(channel_id:, message_id:, emoji:)
|
|
151
|
+
@client.reactions_remove(channel: channel_id, timestamp: message_id, name: emoji)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def open_dm(user_id)
|
|
155
|
+
result = @client.conversations_open(users: user_id)
|
|
156
|
+
result["channel"]["id"]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def fetch_messages(channel_id:, thread_id: nil, cursor: nil, limit: 50)
|
|
160
|
+
result = if thread_id
|
|
161
|
+
@client.conversations_replies(channel: channel_id, ts: thread_id, cursor: cursor, limit: limit)
|
|
162
|
+
else
|
|
163
|
+
@client.conversations_history(channel: channel_id, cursor: cursor, limit: limit)
|
|
164
|
+
end
|
|
165
|
+
messages = result["messages"].map { |m| parse_slack_message(m, channel_id) }
|
|
166
|
+
[messages, result["response_metadata"]&.dig("next_cursor")]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def open_modal(trigger_id:, modal:)
|
|
170
|
+
view = @modal_renderer.render(modal)
|
|
171
|
+
@client.views_open(trigger_id: trigger_id, view: view)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def start_typing(channel_id:, thread_id: nil)
|
|
175
|
+
# Slack doesn't have a native typing indicator API for bots
|
|
176
|
+
# This is a no-op but the capability is declared for streaming support
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def mention(user_id)
|
|
180
|
+
"<@#{user_id}>"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def render(postable_message)
|
|
184
|
+
if postable_message.card?
|
|
185
|
+
@renderer.render(postable_message.card)
|
|
186
|
+
else
|
|
187
|
+
postable_message.text
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
def parse_body(body, content_type)
|
|
194
|
+
if content_type&.include?("application/json")
|
|
195
|
+
JSON.parse(body)
|
|
196
|
+
elsif content_type&.include?("application/x-www-form-urlencoded")
|
|
197
|
+
params = Rack::Utils.parse_query(body)
|
|
198
|
+
if params["payload"]
|
|
199
|
+
JSON.parse(params["payload"])
|
|
200
|
+
else
|
|
201
|
+
params
|
|
202
|
+
end
|
|
203
|
+
else
|
|
204
|
+
begin
|
|
205
|
+
JSON.parse(body)
|
|
206
|
+
rescue JSON::ParserError
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def parse_slack_message(data, channel_id)
|
|
213
|
+
ChatSDK::Message.new(
|
|
214
|
+
id: data["ts"],
|
|
215
|
+
text: data["text"] || "",
|
|
216
|
+
author: ChatSDK::Author.new(
|
|
217
|
+
id: data["user"] || data["bot_id"] || "unknown",
|
|
218
|
+
name: data["username"] || data["user"] || "unknown",
|
|
219
|
+
platform: :slack,
|
|
220
|
+
bot: !!data["bot_id"]
|
|
221
|
+
),
|
|
222
|
+
thread_id: data["thread_ts"] || data["ts"],
|
|
223
|
+
channel_id: channel_id,
|
|
224
|
+
platform: :slack,
|
|
225
|
+
timestamp: data["ts"],
|
|
226
|
+
raw: data
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Slack
|
|
5
|
+
class BlockKitRenderer
|
|
6
|
+
def render(node)
|
|
7
|
+
case node.type
|
|
8
|
+
when :card then render_card(node)
|
|
9
|
+
else [render_node(node)]
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def render_card(node)
|
|
16
|
+
node.children.map { |child| render_node(child) }.compact
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render_node(node)
|
|
20
|
+
case node.type
|
|
21
|
+
when :text then render_text(node)
|
|
22
|
+
when :divider then {type: "divider"}
|
|
23
|
+
when :image then render_image(node)
|
|
24
|
+
when :fields then render_fields(node)
|
|
25
|
+
when :section then render_section(node)
|
|
26
|
+
when :actions then render_actions(node)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def render_text(node)
|
|
31
|
+
{
|
|
32
|
+
type: "section",
|
|
33
|
+
text: {type: "mrkdwn", text: node.attributes[:content]}
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def render_image(node)
|
|
38
|
+
block = {type: "image", image_url: node.attributes[:url]}
|
|
39
|
+
block[:alt_text] = node.attributes[:alt] || " "
|
|
40
|
+
block
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def render_fields(node)
|
|
44
|
+
{
|
|
45
|
+
type: "section",
|
|
46
|
+
fields: node.children.map do |field|
|
|
47
|
+
{type: "mrkdwn", text: "*#{field.attributes[:label]}*\n#{field.attributes[:value]}"}
|
|
48
|
+
end
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def render_section(node)
|
|
53
|
+
block = {type: "section"}
|
|
54
|
+
text_children = node.children.select { |c| c.type == :text }
|
|
55
|
+
if text_children.any?
|
|
56
|
+
block[:text] = {type: "mrkdwn", text: text_children.map { |t| t.attributes[:content] }.join("\n")}
|
|
57
|
+
end
|
|
58
|
+
block
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def render_actions(node)
|
|
62
|
+
{
|
|
63
|
+
type: "actions",
|
|
64
|
+
elements: node.children.map { |child| render_action_element(child) }.compact
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def render_action_element(node)
|
|
69
|
+
case node.type
|
|
70
|
+
when :button then render_button(node)
|
|
71
|
+
when :link_button then render_link_button(node)
|
|
72
|
+
when :select then render_select(node)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render_button(node)
|
|
77
|
+
btn = {
|
|
78
|
+
type: "button",
|
|
79
|
+
text: {type: "plain_text", text: node.attributes[:text]},
|
|
80
|
+
action_id: node.attributes[:id]
|
|
81
|
+
}
|
|
82
|
+
btn[:value] = node.attributes[:value] if node.attributes[:value]
|
|
83
|
+
if node.attributes[:style] == :primary
|
|
84
|
+
btn[:style] = "primary"
|
|
85
|
+
elsif node.attributes[:style] == :danger
|
|
86
|
+
btn[:style] = "danger"
|
|
87
|
+
end
|
|
88
|
+
btn
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def render_link_button(node)
|
|
92
|
+
{
|
|
93
|
+
type: "button",
|
|
94
|
+
text: {type: "plain_text", text: node.attributes[:text]},
|
|
95
|
+
url: node.attributes[:url],
|
|
96
|
+
action_id: "link_#{node.attributes[:url].hash.abs}"
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def render_select(node)
|
|
101
|
+
sel = {
|
|
102
|
+
type: "static_select",
|
|
103
|
+
action_id: node.attributes[:id],
|
|
104
|
+
options: node.children.map { |opt| render_option(opt) }
|
|
105
|
+
}
|
|
106
|
+
if node.attributes[:placeholder]
|
|
107
|
+
sel[:placeholder] = {type: "plain_text", text: node.attributes[:placeholder]}
|
|
108
|
+
end
|
|
109
|
+
sel
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def render_option(node)
|
|
113
|
+
opt = {
|
|
114
|
+
text: {type: "plain_text", text: node.attributes[:text]},
|
|
115
|
+
value: node.attributes[:value]
|
|
116
|
+
}
|
|
117
|
+
if node.attributes[:description]
|
|
118
|
+
opt[:description] = {type: "plain_text", text: node.attributes[:description]}
|
|
119
|
+
end
|
|
120
|
+
opt
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Slack
|
|
5
|
+
class EventParser
|
|
6
|
+
class << self
|
|
7
|
+
def parse(payload)
|
|
8
|
+
case payload["type"]
|
|
9
|
+
when "event_callback"
|
|
10
|
+
parse_event_callback(payload)
|
|
11
|
+
when "block_actions", "interactive_message"
|
|
12
|
+
parse_block_actions(payload)
|
|
13
|
+
when "view_submission"
|
|
14
|
+
parse_view_submission(payload)
|
|
15
|
+
when "slash_commands"
|
|
16
|
+
parse_slash_command(payload)
|
|
17
|
+
else
|
|
18
|
+
# Slash commands come as form-encoded, not wrapped in type
|
|
19
|
+
if payload["command"]
|
|
20
|
+
parse_slash_command(payload)
|
|
21
|
+
else
|
|
22
|
+
[]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def parse_event_callback(payload)
|
|
30
|
+
event = payload["event"]
|
|
31
|
+
return [] unless event
|
|
32
|
+
|
|
33
|
+
case event["type"]
|
|
34
|
+
when "app_mention"
|
|
35
|
+
parse_mention(event, payload)
|
|
36
|
+
when "message"
|
|
37
|
+
parse_message_event(event, payload)
|
|
38
|
+
when "reaction_added"
|
|
39
|
+
parse_reaction(event, payload, added: true)
|
|
40
|
+
when "reaction_removed"
|
|
41
|
+
parse_reaction(event, payload, added: false)
|
|
42
|
+
else
|
|
43
|
+
[]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parse_mention(event, payload)
|
|
48
|
+
author = ChatSDK::Author.new(
|
|
49
|
+
id: event["user"],
|
|
50
|
+
name: event["user"],
|
|
51
|
+
platform: :slack
|
|
52
|
+
)
|
|
53
|
+
message = ChatSDK::Message.new(
|
|
54
|
+
id: event["ts"],
|
|
55
|
+
text: event["text"] || "",
|
|
56
|
+
author: author,
|
|
57
|
+
thread_id: event["thread_ts"] || event["ts"],
|
|
58
|
+
channel_id: event["channel"],
|
|
59
|
+
platform: :slack,
|
|
60
|
+
raw: event
|
|
61
|
+
)
|
|
62
|
+
[ChatSDK::Events::Mention.new(
|
|
63
|
+
message: message,
|
|
64
|
+
thread_id: event["thread_ts"] || event["ts"],
|
|
65
|
+
channel_id: event["channel"],
|
|
66
|
+
platform: :slack,
|
|
67
|
+
adapter_name: :slack,
|
|
68
|
+
raw: payload
|
|
69
|
+
)]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def parse_message_event(event, payload)
|
|
73
|
+
return [] if event["subtype"] && event["subtype"] != "file_share"
|
|
74
|
+
return [] if event["bot_id"]
|
|
75
|
+
|
|
76
|
+
author = ChatSDK::Author.new(
|
|
77
|
+
id: event["user"],
|
|
78
|
+
name: event["user"],
|
|
79
|
+
platform: :slack
|
|
80
|
+
)
|
|
81
|
+
message = ChatSDK::Message.new(
|
|
82
|
+
id: event["ts"],
|
|
83
|
+
text: event["text"] || "",
|
|
84
|
+
author: author,
|
|
85
|
+
thread_id: event["thread_ts"] || event["ts"],
|
|
86
|
+
channel_id: event["channel"],
|
|
87
|
+
platform: :slack,
|
|
88
|
+
raw: event
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
channel_type = event["channel_type"]
|
|
92
|
+
if channel_type == "im"
|
|
93
|
+
[ChatSDK::Events::DirectMessage.new(
|
|
94
|
+
message: message,
|
|
95
|
+
thread_id: event["thread_ts"] || event["ts"],
|
|
96
|
+
channel_id: event["channel"],
|
|
97
|
+
platform: :slack,
|
|
98
|
+
adapter_name: :slack,
|
|
99
|
+
raw: payload
|
|
100
|
+
)]
|
|
101
|
+
else
|
|
102
|
+
[ChatSDK::Events::SubscribedMessage.new(
|
|
103
|
+
message: message,
|
|
104
|
+
thread_id: event["thread_ts"] || event["ts"],
|
|
105
|
+
channel_id: event["channel"],
|
|
106
|
+
platform: :slack,
|
|
107
|
+
adapter_name: :slack,
|
|
108
|
+
raw: payload
|
|
109
|
+
)]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def parse_reaction(event, payload, added:)
|
|
114
|
+
[ChatSDK::Events::Reaction.new(
|
|
115
|
+
emoji: event["reaction"],
|
|
116
|
+
user_id: event["user"],
|
|
117
|
+
message_id: event.dig("item", "ts"),
|
|
118
|
+
thread_id: event.dig("item", "ts"),
|
|
119
|
+
channel_id: event.dig("item", "channel"),
|
|
120
|
+
added: added,
|
|
121
|
+
platform: :slack,
|
|
122
|
+
adapter_name: :slack,
|
|
123
|
+
raw: payload
|
|
124
|
+
)]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def parse_block_actions(payload)
|
|
128
|
+
actions = payload["actions"] || []
|
|
129
|
+
user = payload["user"]
|
|
130
|
+
channel = payload.dig("channel", "id")
|
|
131
|
+
message_ts = payload.dig("message", "ts")
|
|
132
|
+
thread_ts = payload.dig("message", "thread_ts") || message_ts
|
|
133
|
+
trigger_id = payload["trigger_id"]
|
|
134
|
+
|
|
135
|
+
actions.map do |action|
|
|
136
|
+
ChatSDK::Events::Action.new(
|
|
137
|
+
action_id: action["action_id"],
|
|
138
|
+
value: action["value"] || action.dig("selected_option", "value"),
|
|
139
|
+
user: ChatSDK::Author.new(id: user["id"], name: user["name"] || user["id"], platform: :slack),
|
|
140
|
+
thread_id: thread_ts,
|
|
141
|
+
channel_id: channel,
|
|
142
|
+
trigger_id: trigger_id,
|
|
143
|
+
platform: :slack,
|
|
144
|
+
adapter_name: :slack,
|
|
145
|
+
raw: payload
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def parse_view_submission(payload)
|
|
151
|
+
# View submissions are handled differently - return empty for now
|
|
152
|
+
# They'll be handled via on_modal_submit in a future version
|
|
153
|
+
[]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def parse_slash_command(payload)
|
|
157
|
+
[ChatSDK::Events::SlashCommand.new(
|
|
158
|
+
command: payload["command"],
|
|
159
|
+
text: payload["text"] || "",
|
|
160
|
+
user_id: payload["user_id"],
|
|
161
|
+
channel_id: payload["channel_id"],
|
|
162
|
+
trigger_id: payload["trigger_id"],
|
|
163
|
+
platform: :slack,
|
|
164
|
+
adapter_name: :slack,
|
|
165
|
+
raw: payload
|
|
166
|
+
)]
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatSDK
|
|
4
|
+
module Slack
|
|
5
|
+
class ModalRenderer
|
|
6
|
+
def render(node)
|
|
7
|
+
view = {
|
|
8
|
+
type: "modal",
|
|
9
|
+
title: {type: "plain_text", text: node.attributes[:title]}
|
|
10
|
+
}
|
|
11
|
+
view[:callback_id] = node.attributes[:callback_id] if node.attributes[:callback_id]
|
|
12
|
+
if node.attributes[:submit_label]
|
|
13
|
+
view[:submit] = {type: "plain_text", text: node.attributes[:submit_label]}
|
|
14
|
+
end
|
|
15
|
+
view[:blocks] = node.children.map { |child| render_block(child) }.compact
|
|
16
|
+
view
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def render_block(node)
|
|
22
|
+
case node.type
|
|
23
|
+
when :input then render_input(node)
|
|
24
|
+
when :text then render_text(node)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render_input(node)
|
|
29
|
+
block = {
|
|
30
|
+
type: "input",
|
|
31
|
+
block_id: node.attributes[:id],
|
|
32
|
+
label: {type: "plain_text", text: node.attributes[:label]},
|
|
33
|
+
optional: node.attributes[:optional] || false
|
|
34
|
+
}
|
|
35
|
+
block[:element] = case node.attributes[:input_type]
|
|
36
|
+
when :text
|
|
37
|
+
el = {
|
|
38
|
+
type: "plain_text_input",
|
|
39
|
+
action_id: node.attributes[:id],
|
|
40
|
+
multiline: !!node.attributes[:multiline]
|
|
41
|
+
}
|
|
42
|
+
el[:placeholder] = {type: "plain_text", text: node.attributes[:placeholder]} if node.attributes[:placeholder]
|
|
43
|
+
el
|
|
44
|
+
when :select
|
|
45
|
+
el = {
|
|
46
|
+
type: "static_select",
|
|
47
|
+
action_id: node.attributes[:id],
|
|
48
|
+
options: node.children.map { |opt| render_option(opt) }
|
|
49
|
+
}
|
|
50
|
+
el[:placeholder] = {type: "plain_text", text: node.attributes[:placeholder]} if node.attributes[:placeholder]
|
|
51
|
+
el
|
|
52
|
+
end
|
|
53
|
+
block
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def render_text(node)
|
|
57
|
+
{
|
|
58
|
+
type: "section",
|
|
59
|
+
text: {type: "mrkdwn", text: node.attributes[:content]}
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_option(node)
|
|
64
|
+
opt = {
|
|
65
|
+
text: {type: "plain_text", text: node.attributes[:text]},
|
|
66
|
+
value: node.attributes[:value]
|
|
67
|
+
}
|
|
68
|
+
opt[:description] = {type: "plain_text", text: node.attributes[:description]} if node.attributes[:description]
|
|
69
|
+
opt
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "chat_sdk"
|
|
4
|
+
require "slack-ruby-client"
|
|
5
|
+
require "zeitwerk"
|
|
6
|
+
|
|
7
|
+
module ChatSDK
|
|
8
|
+
module Slack
|
|
9
|
+
class << self
|
|
10
|
+
def loader
|
|
11
|
+
@loader ||= begin
|
|
12
|
+
loader = Zeitwerk::Loader.new
|
|
13
|
+
loader.tag = "chat_sdk-slack"
|
|
14
|
+
loader.inflector.inflect("chat_sdk" => "ChatSDK")
|
|
15
|
+
loader.push_dir("#{__dir__}/slack", namespace: ChatSDK::Slack)
|
|
16
|
+
loader.setup
|
|
17
|
+
loader
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
ChatSDK::Slack.loader
|
metadata
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: chat_sdk-slack
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Rootly
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: chat_sdk
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: slack-ruby-client
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
description: Slack bot adapter for the ChatSDK framework using slack-ruby-client
|
|
41
|
+
email:
|
|
42
|
+
- eng@rootly.com
|
|
43
|
+
executables: []
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- lib/chat_sdk/slack.rb
|
|
48
|
+
- lib/chat_sdk/slack/adapter.rb
|
|
49
|
+
- lib/chat_sdk/slack/block_kit_renderer.rb
|
|
50
|
+
- lib/chat_sdk/slack/event_parser.rb
|
|
51
|
+
- lib/chat_sdk/slack/modal_renderer.rb
|
|
52
|
+
homepage: https://github.com/rootlyhq/rootly-chat-sdk
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
rubygems_mfa_required: 'true'
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: 3.2.0
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 4.0.16
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: Slack adapter for ChatSDK
|
|
74
|
+
test_files: []
|