chat_sdk-slack 1.0.1 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3267f830fd79846f929e7e61be15c2b65d1e6183f6f5d56271f88fbb6fdf85b
4
- data.tar.gz: 9fe0cd67184ef4561a2e6ccb6d5678f5a639f2b53add2be5ef6436b239a1a7c3
3
+ metadata.gz: d93aacf823c427212c01efbdf76255caf3cd4a534b033b5557a9bbff9aaa8b97
4
+ data.tar.gz: a82ac7a769d42943060042cfaf16179b1a7c3482d9bfd9b268631b59a9723fa8
5
5
  SHA512:
6
- metadata.gz: c686e9ac2a237c82166200783289f2310cb57cc3760a4d55b11447fa0f7a746b10ffb40b713d18334f277f3efb91663733977f082cf0152ee96ed387ddbd2ed1
7
- data.tar.gz: 1e897869f36b3571e31dea33be0e78f9bd46bcbc5e3c839da11453337242378933dcfec8eec289ceb4c6cbae4c339bce0a1c4399f39fe74a7980703f5e1e4c40
6
+ metadata.gz: 49b4f95d68bb451922802284fae8ef36f9dffbc72ae33c44943618e5ca4141410c9f58cc885d353b96a2004ee12544cab4739cfeb0c3ef8fc2557c8bb731f3d1
7
+ data.tar.gz: f1ad880423c30b689eff5476070d3227310752b3122fd97c17a3fe34d8b52a3b0e05f7c67688b1bb47511dc839c0a1704fc3cfb210395d6654a966e119ff199e
@@ -9,11 +9,12 @@ module ChatSDK
9
9
  :scheduled_messages
10
10
 
11
11
  def initialize(bot_token: nil, signing_secret: nil,
12
- client_id: nil, client_secret: nil)
12
+ client_id: nil, client_secret: nil, proxy: nil)
13
13
  @bot_token = bot_token || ENV["SLACK_BOT_TOKEN"]
14
14
  @signing_secret = signing_secret || ENV["SLACK_SIGNING_SECRET"]
15
15
  @client_id = client_id || ENV["SLACK_CLIENT_ID"]
16
16
  @client_secret = client_secret || ENV["SLACK_CLIENT_SECRET"]
17
+ @proxy = proxy || ENV["HTTPS_PROXY"] || ENV["https_proxy"]
17
18
 
18
19
  unless @bot_token || @client_id
19
20
  raise ChatSDK::ConfigurationError, "Slack bot_token or client_id required"
@@ -24,7 +25,7 @@ module ChatSDK
24
25
  ::Slack.configure do |config|
25
26
  config.token = @bot_token
26
27
  end
27
- @client = ::Slack::Web::Client.new(token: @bot_token)
28
+ @client = build_client(token: @bot_token)
28
29
  end
29
30
 
30
31
  @renderer = BlockKitRenderer.new
@@ -71,7 +72,7 @@ module ChatSDK
71
72
  def handle_oauth_callback(code:, redirect_uri: nil)
72
73
  raise ChatSDK::ConfigurationError, "client_id required for OAuth" unless @client_id
73
74
 
74
- temp_client = ::Slack::Web::Client.new
75
+ temp_client = build_client
75
76
  params = {
76
77
  client_id: @client_id,
77
78
  client_secret: @client_secret,
@@ -353,6 +354,13 @@ module ChatSDK
353
354
 
354
355
  private
355
356
 
357
+ def build_client(token: nil)
358
+ options = {}
359
+ options[:token] = token if token
360
+ options[:proxy] = @proxy if @proxy
361
+ options.empty? ? ::Slack::Web::Client.new : ::Slack::Web::Client.new(options)
362
+ end
363
+
356
364
  def resolve_team_client(team_id)
357
365
  return unless @client_id && @state
358
366
 
@@ -365,7 +373,7 @@ module ChatSDK
365
373
  installation = get_installation(team_id)
366
374
  return unless installation
367
375
 
368
- new_client = ::Slack::Web::Client.new(token: installation["bot_token"])
376
+ new_client = build_client(token: installation["bot_token"])
369
377
  @team_clients[team_id] = new_client
370
378
  ::Thread.current[:chat_sdk_slack_client] = new_client
371
379
  end
@@ -13,6 +13,8 @@ module ChatSDK
13
13
  private
14
14
 
15
15
  def render_card(node)
16
+ @used_native_table = false
17
+ @chart_count = 0
16
18
  node.children.map { |child| render_node(child) }.compact
17
19
  end
18
20
 
@@ -24,6 +26,8 @@ module ChatSDK
24
26
  when :fields then render_fields(node)
25
27
  when :section then render_section(node)
26
28
  when :actions then render_actions(node)
29
+ when :table then render_table(node)
30
+ when :chart then render_chart(node)
27
31
  end
28
32
  end
29
33
 
@@ -93,7 +97,7 @@ module ChatSDK
93
97
  type: "button",
94
98
  text: {type: "plain_text", text: node.attributes[:text]},
95
99
  url: node.attributes[:url],
96
- action_id: "link_#{node.attributes[:url].hash.abs}"
100
+ action_id: node.attributes[:id] || "link_#{node.attributes[:url].hash.abs}"
97
101
  }
98
102
  end
99
103
 
@@ -119,6 +123,75 @@ module ChatSDK
119
123
  end
120
124
  opt
121
125
  end
126
+
127
+ def render_table(node)
128
+ headers = Array(node.attributes[:headers])
129
+ rows = Array(node.attributes[:rows])
130
+ char_count = (headers + rows.flatten).sum { |cell| cell.to_s.length }
131
+ if @used_native_table || rows.length > 100 || headers.length > 20 || char_count > 10_000
132
+ return fallback_block(ChatSDK::Cards::Renderer.new.render(node))
133
+ end
134
+
135
+ @used_native_table = true
136
+ rendered_rows = [headers, *rows].map do |row|
137
+ Array(row).map { |cell| {type: "raw_text", text: cell.to_s.empty? ? " " : cell.to_s} }
138
+ end
139
+ return {type: "table", rows: rendered_rows} if rows.empty?
140
+
141
+ block = {
142
+ type: "data_table",
143
+ caption: node.attributes[:caption] || "Table",
144
+ rows: rendered_rows
145
+ }
146
+ block[:page_size] = node.attributes[:page_size].to_i.clamp(1, 100) if node.attributes[:page_size]
147
+ block
148
+ end
149
+
150
+ def render_chart(node)
151
+ return fallback_block(node.fallback_text) if @chart_count >= 2
152
+
153
+ block = chart_block(node)
154
+ return fallback_block(node.fallback_text) unless block
155
+
156
+ @chart_count += 1
157
+ block
158
+ end
159
+
160
+ def chart_block(node)
161
+ title = node.attributes[:title].to_s
162
+ return if title.empty? || title.length > 50
163
+
164
+ type = node.attributes[:chart_type].to_sym
165
+ if type == :pie
166
+ segments = Array(node.attributes[:segments])
167
+ return unless segments.length.between?(1, 12) && segments.all? { |segment| valid_chart_label?(value_for(segment, :label)) && value_for(segment, :value).to_f.positive? }
168
+
169
+ return {type: "data_visualization", title: title, chart: {type: "pie", segments: segments}}
170
+ end
171
+
172
+ categories = Array(node.attributes[:categories])
173
+ series = Array(node.attributes[:series])
174
+ return unless %i[bar area line].include?(type)
175
+ return unless categories.length.between?(1, 20) && categories.all? { |category| valid_chart_label?(category) }
176
+ return unless series.length.between?(1, 12) && series.all? { |item| valid_chart_label?(value_for(item, :name)) }
177
+
178
+ axis = {categories: categories}
179
+ axis[:x_label] = node.attributes[:x_label] if node.attributes[:x_label]
180
+ axis[:y_label] = node.attributes[:y_label] if node.attributes[:y_label]
181
+ {type: "data_visualization", title: title, chart: {type: type.to_s, series: series, axis_config: axis}}
182
+ end
183
+
184
+ def valid_chart_label?(label)
185
+ label.to_s.length.between?(1, 20)
186
+ end
187
+
188
+ def value_for(hash, key)
189
+ hash[key] || hash[key.to_s]
190
+ end
191
+
192
+ def fallback_block(text)
193
+ {type: "section", text: {type: "mrkdwn", text: "```#{text}```"}}
194
+ end
122
195
  end
123
196
  end
124
197
  end
@@ -68,6 +68,8 @@ module ChatSDK
68
68
  end
69
69
 
70
70
  def parse_message_event(event, payload)
71
+ return parse_message_updated(event, payload) if event["subtype"] == "message_changed"
72
+ return parse_message_deleted(event, payload) if event["subtype"] == "message_deleted"
71
73
  return [] if event["subtype"] && event["subtype"] != "file_share"
72
74
  return [] if event["bot_id"]
73
75
 
@@ -108,6 +110,72 @@ module ChatSDK
108
110
  end
109
111
  end
110
112
 
113
+ def parse_message_updated(event, payload)
114
+ return [] if event["hidden"]
115
+
116
+ message_data = event["message"]
117
+ return [] unless message_data
118
+
119
+ channel_id = event["channel"] || message_data["channel"]
120
+ message = slack_message(message_data, channel_id)
121
+ previous = slack_message(event["previous_message"], channel_id) if event["previous_message"]
122
+ [ChatSDK::Events::MessageUpdated.new(
123
+ message: message,
124
+ previous_message: previous,
125
+ thread_id: message.thread_id,
126
+ channel_id: channel_id,
127
+ platform: :slack,
128
+ adapter_name: :slack,
129
+ raw: payload
130
+ )]
131
+ end
132
+
133
+ def parse_message_deleted(event, payload)
134
+ previous = event["previous_message"]
135
+ message_id = event["deleted_ts"] || previous&.dig("ts")
136
+ return [] unless message_id
137
+
138
+ channel_id = event["channel"] || previous&.dig("channel")
139
+ previous_message = slack_message(previous, channel_id) if previous
140
+ thread_id = previous&.dig("thread_ts") || message_id
141
+ [ChatSDK::Events::MessageDeleted.new(
142
+ message_id: message_id,
143
+ previous_message: previous_message,
144
+ thread_id: thread_id,
145
+ channel_id: channel_id,
146
+ deleted_at: slack_time(event["event_ts"] || event["ts"]),
147
+ platform: :slack,
148
+ adapter_name: :slack,
149
+ raw: payload
150
+ )]
151
+ end
152
+
153
+ def slack_message(data, channel_id)
154
+ user_id = data["user"] || data.dig("bot_profile", "user_id") || "unknown"
155
+ ChatSDK::Message.new(
156
+ id: data["ts"],
157
+ text: data["text"] || "",
158
+ author: ChatSDK::Author.new(
159
+ id: user_id,
160
+ name: data["username"] || user_id,
161
+ platform: :slack,
162
+ bot: !!data["bot_id"],
163
+ system: user_id == "USLACK"
164
+ ),
165
+ thread_id: data["thread_ts"] || data["ts"],
166
+ channel_id: channel_id,
167
+ platform: :slack,
168
+ timestamp: slack_time(data["ts"]),
169
+ raw: data
170
+ )
171
+ end
172
+
173
+ def slack_time(value)
174
+ Time.at(Float(value)) if value
175
+ rescue ArgumentError, TypeError
176
+ nil
177
+ end
178
+
111
179
  def parse_reaction(event, payload, added:)
112
180
  [ChatSDK::Events::Reaction.new(
113
181
  emoji: event["reaction"],
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
4
+
3
5
  module ChatSDK
4
6
  module Slack
5
7
  class ModalRenderer
@@ -49,10 +51,37 @@ module ChatSDK
49
51
  }
50
52
  el[:placeholder] = {type: "plain_text", text: node.attributes[:placeholder]} if node.attributes[:placeholder]
51
53
  el
54
+ when :date
55
+ el = {type: "datepicker", action_id: node.attributes[:id]}
56
+ initial_date = valid_initial_date(node.attributes[:initial_value])
57
+ el[:initial_date] = initial_date if initial_date
58
+ el[:placeholder] = {type: "plain_text", text: node.attributes[:placeholder]} if node.attributes[:placeholder]
59
+ el
60
+ when :number
61
+ el = {
62
+ type: "number_input",
63
+ action_id: node.attributes[:id],
64
+ is_decimal_allowed: !!node.attributes[:decimal]
65
+ }
66
+ el[:min_value] = node.attributes[:min].to_s unless node.attributes[:min].nil?
67
+ el[:max_value] = node.attributes[:max].to_s unless node.attributes[:max].nil?
68
+ el[:initial_value] = node.attributes[:initial_value].to_s unless node.attributes[:initial_value].nil?
69
+ el[:placeholder] = {type: "plain_text", text: node.attributes[:placeholder]} if node.attributes[:placeholder]
70
+ el
52
71
  end
53
72
  block
54
73
  end
55
74
 
75
+ def valid_initial_date(value)
76
+ return unless value
77
+
78
+ parsed = Date.iso8601(value.to_s)
79
+ value.to_s if parsed.iso8601 == value.to_s
80
+ rescue Date::Error
81
+ ChatSDK::Log.warn("Ignoring invalid modal initial date: #{value.inspect}")
82
+ nil
83
+ end
84
+
56
85
  def render_text(node)
57
86
  {
58
87
  type: "section",
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: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Quentin Rousseau