collavre_slack 0.2.4 → 0.2.6

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: 9a8e800e1e2f729af9f99e0ebedc8e070723ba61f194ee9df766fcdfdd27c933
4
- data.tar.gz: 2737d507f261c19b3446afd84f8747d21cb849fcddfa5d139b2dbd9a9a7f96ad
3
+ metadata.gz: 8d470cccf0cf90daff6918a44ff263700803a3a51114003be481d51fc38431f9
4
+ data.tar.gz: 63c5cbebe3fd89218f494a41530e190d1ba85ca419e1baccc45914a40c6995aa
5
5
  SHA512:
6
- metadata.gz: c66ed359c01fc4d3c8f95badd2a9b692c01369187da548f5c9620fde05c7391c8e9c9011b8ba5f98f5f3bcb6925d97b2491992a38258a85b1a6999f330180bb0
7
- data.tar.gz: a86dad280226ab1c787e9c959b134fa4b1dde7245726a748620a396058171a3c21d2b469034c2bd2444fa9fbfc493d37b9980bf606a0f6ac2af6fad776426265
6
+ metadata.gz: 982316ca1c8c573d38b8f5c95003da48503153067be902844c9462458df362b46d6c2cc9188134d20cec78426355058ded4a54d155e17e3e966eef16a7646ba5
7
+ data.tar.gz: 5252ff282030eba366c99c0dbc9c11bb356c824fd65b505d37d0234b51d15fb35be6b68de435713ecd5739573963f0fa5fa689eb09a8cf08113a2132aee476c9
@@ -1,12 +1,14 @@
1
1
  module CollavreSlack
2
2
  module Creatives
3
3
  class SlackIntegrationsController < ApplicationController
4
+ include Collavre::IntegrationSetup
5
+ include Collavre::IntegrationPermission
6
+
4
7
  before_action :set_creative
8
+ before_action :set_origin
5
9
 
6
10
  def index
7
- # Always use origin creative for Slack links (chat messages are on origin)
8
- target_creative = @creative.effective_origin
9
- @links = SlackChannelLink.where(creative: target_creative)
11
+ @links = SlackChannelLink.where(creative: @origin)
10
12
  respond_to do |format|
11
13
  format.json do
12
14
  slack_account = Current.user ? SlackAccount.find_by(user: Current.user) : nil
@@ -31,13 +33,13 @@ module CollavreSlack
31
33
 
32
34
  def create
33
35
  unless @creative.has_permission?(Current.user, :feedback)
34
- render json: { success: false, error: I18n.t("collavre_slack.errors.forbidden") }, status: :forbidden
36
+ render json: { success: false, error: integration_forbidden_message }, status: :forbidden
35
37
  return
36
38
  end
37
39
 
38
40
  # Find the user's Slack account
39
41
  slack_account = if params[:slack_account_id].present?
40
- SlackAccount.find(params[:slack_account_id])
42
+ SlackAccount.find_by!(id: params[:slack_account_id], user: Current.user)
41
43
  else
42
44
  SlackAccount.find_by(user: Current.user)
43
45
  end
@@ -47,11 +49,8 @@ module CollavreSlack
47
49
  return
48
50
  end
49
51
 
50
- # Always link to origin creative (chat messages are on origin)
51
- target_creative = @creative.effective_origin
52
-
53
52
  service = SlackIntegrationService.new(user: Current.user, slack_account: slack_account)
54
- link = service.link_channel(creative: target_creative, channel_id: params[:channel_id], channel_name: params[:channel_name])
53
+ link = service.link_channel(creative: @origin, channel_id: params[:channel_id], channel_name: params[:channel_name])
55
54
 
56
55
  if link.persisted?
57
56
  render json: { success: true, link: { id: link.id, channel_id: link.channel_id, channel_name: link.channel_name } }, status: :created
@@ -60,10 +59,8 @@ module CollavreSlack
60
59
  end
61
60
  end
62
61
 
63
- # Lightweight endpoint for badge display — returns only existing links, no Slack API calls
64
62
  def badge
65
- target_creative = @creative.effective_origin
66
- links = SlackChannelLink.where(creative: target_creative)
63
+ links = SlackChannelLink.where(creative: @origin)
67
64
  render json: {
68
65
  links: links.map { |link|
69
66
  { channel_name: link.channel_name }
@@ -73,15 +70,13 @@ module CollavreSlack
73
70
 
74
71
  def destroy
75
72
  link = SlackChannelLink.find(params[:id])
76
- # Check against origin creative
77
- target_creative = @creative.effective_origin
78
- unless link.creative_id == target_creative.id
73
+ unless link.creative_id == @origin.id
79
74
  render json: { success: false, error: I18n.t("collavre_slack.errors.not_found") }, status: :not_found
80
75
  return
81
76
  end
82
77
 
83
78
  unless @creative.has_permission?(Current.user, :feedback)
84
- render json: { success: false, error: I18n.t("collavre_slack.errors.forbidden") }, status: :forbidden
79
+ render json: { success: false, error: integration_forbidden_message }, status: :forbidden
85
80
  return
86
81
  end
87
82
 
@@ -91,8 +86,8 @@ module CollavreSlack
91
86
 
92
87
  private
93
88
 
94
- def set_creative
95
- @creative = Collavre::Creative.find(params[:creative_id])
89
+ def integration_forbidden_message
90
+ I18n.t("collavre_slack.errors.forbidden")
96
91
  end
97
92
 
98
93
  def fetch_channels(slack_account)
@@ -65,7 +65,7 @@ module CollavreSlack
65
65
 
66
66
  if account.save
67
67
  # Render HTML that closes the popup and notifies the parent window
68
- render html: close_popup_html.html_safe, layout: false
68
+ render template: "collavre_slack/slack_auth/callback_success", layout: false
69
69
  else
70
70
  Rails.logger.error("[SlackAuth] Failed to save account: #{account.errors.full_messages.join(', ')}")
71
71
  render json: { error: I18n.t("collavre_slack.errors.save_failed", errors: account.errors.full_messages.join(", ")) }, status: :unprocessable_entity
@@ -81,25 +81,5 @@ module CollavreSlack
81
81
  def message_verifier
82
82
  @message_verifier ||= Rails.application.message_verifier("slack_oauth")
83
83
  end
84
-
85
- def close_popup_html
86
- title = I18n.t("collavre_slack.views.auth.success_title")
87
- message = I18n.t("collavre_slack.views.auth.success_message")
88
- <<~HTML
89
- <!DOCTYPE html>
90
- <html>
91
- <head><title>#{title}</title></head>
92
- <body>
93
- <p>#{message}</p>
94
- <script>
95
- if (window.opener) {
96
- window.opener.postMessage({ type: 'slack-auth-success' }, '*');
97
- }
98
- window.close();
99
- </script>
100
- </body>
101
- </html>
102
- HTML
103
- end
104
84
  end
105
85
  end
@@ -1,4 +1,5 @@
1
1
  import { filterChannels, reconcileSelection, buildChannelViewModels } from './slack_channel_list.js';
2
+ import { csrfToken, showError, clearError, updateStepVisibility, openOAuthPopup, fetchWithCsrf, setupModalClose } from 'collavre/modules/integration_wizard';
2
3
 
3
4
  let slackIntegrationInitialized = false;
4
5
 
@@ -35,10 +36,6 @@ if (!slackIntegrationInitialized) {
35
36
  let selectedChannel = null;
36
37
  let existingLinks = [];
37
38
 
38
- function csrfToken() {
39
- return document.querySelector('meta[name="csrf-token"]')?.content;
40
- }
41
-
42
39
  function resetWizard() {
43
40
  currentStep = 'connect';
44
41
  hasExistingIntegration = false;
@@ -46,8 +43,7 @@ if (!slackIntegrationInitialized) {
46
43
  selectedChannel = null;
47
44
  existingLinks = [];
48
45
  statusEl.textContent = '';
49
- errorEl.style.display = 'none';
50
- errorEl.textContent = '';
46
+ clearError(errorEl);
51
47
  if (connectedStatus) {
52
48
  connectedStatus.style.display = 'none';
53
49
  }
@@ -69,12 +65,7 @@ if (!slackIntegrationInitialized) {
69
65
  }
70
66
 
71
67
  function updateStep() {
72
- ['connect', 'channels', 'summary']
73
- .forEach(function (step) {
74
- const el = document.getElementById(`slack-step-${step}`);
75
- if (!el) return;
76
- el.style.display = (step === currentStep) ? 'block' : 'none';
77
- });
68
+ updateStepVisibility(currentStep, ['connect', 'channels', 'summary'], 'slack-step');
78
69
 
79
70
  if (currentStep === 'connect') {
80
71
  prevBtn.style.display = 'none';
@@ -99,29 +90,13 @@ if (!slackIntegrationInitialized) {
99
90
  }
100
91
  }
101
92
 
102
- function showError(message) {
103
- errorEl.textContent = message;
104
- errorEl.style.display = 'block';
105
- }
106
-
107
- function clearError() {
108
- errorEl.style.display = 'none';
109
- errorEl.textContent = '';
110
- }
111
-
112
93
  function loadIntegrationStatus() {
113
94
  if (!creativeId) return;
114
95
 
115
96
  statusEl.textContent = modal.dataset.loading || 'Loading...';
116
- clearError();
97
+ clearError(errorEl);
117
98
 
118
- fetch(`/slack/creatives/${creativeId}/slack_integrations`, {
119
- method: 'GET',
120
- headers: {
121
- 'X-CSRF-Token': csrfToken(),
122
- 'Accept': 'application/json'
123
- }
124
- })
99
+ fetchWithCsrf(`/slack/creatives/${creativeId}/slack_integrations`)
125
100
  .then(response => response.json())
126
101
  .then(data => {
127
102
  console.log('Slack integration status:', data);
@@ -147,7 +122,7 @@ if (!slackIntegrationInitialized) {
147
122
  .catch(error => {
148
123
  console.error('Error loading integration status:', error);
149
124
  statusEl.textContent = '';
150
- showError('Failed to load integration status');
125
+ showError(errorEl, 'Failed to load integration status');
151
126
  });
152
127
  }
153
128
 
@@ -160,7 +135,9 @@ if (!slackIntegrationInitialized) {
160
135
  li.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:0.5em 0.75em;margin-bottom:0.5em;background:var(--color-bg-alt);border-radius:4px;';
161
136
 
162
137
  const channelInfo = document.createElement('div');
163
- channelInfo.innerHTML = `<strong>#${link.channel_name || link.channel_id}</strong>`;
138
+ const channelStrong = document.createElement('strong');
139
+ channelStrong.textContent = `#${link.channel_name || link.channel_id}`;
140
+ channelInfo.appendChild(channelStrong);
164
141
 
165
142
  if (link.last_synced_at) {
166
143
  const syncInfo = document.createElement('span');
@@ -218,15 +195,9 @@ if (!slackIntegrationInitialized) {
218
195
  const btn = event.target;
219
196
  btn.disabled = true;
220
197
  btn.textContent = '...';
221
- clearError();
198
+ clearError(errorEl);
222
199
 
223
- fetch(`/slack/creatives/${creativeId}/slack_integrations/${link.id}`, {
224
- method: 'DELETE',
225
- headers: {
226
- 'X-CSRF-Token': csrfToken(),
227
- 'Accept': 'application/json'
228
- }
229
- })
200
+ fetchWithCsrf(`/slack/creatives/${creativeId}/slack_integrations/${link.id}`, { method: 'DELETE' })
230
201
  .then(response => response.json())
231
202
  .then(data => {
232
203
  if (data.success) {
@@ -240,14 +211,14 @@ if (!slackIntegrationInitialized) {
240
211
  statusEl.textContent = modal.dataset.deleteSuccess || 'Channel link removed';
241
212
  statusEl.style.color = 'green';
242
213
  } else {
243
- showError(data.message || data.error || 'Deletion failed');
214
+ showError(errorEl, data.message || data.error || 'Deletion failed');
244
215
  btn.disabled = false;
245
216
  btn.textContent = modal.dataset.deleteButtonLabel || 'Remove';
246
217
  }
247
218
  })
248
219
  .catch(error => {
249
220
  console.error('Delete error:', error);
250
- showError('Deletion failed');
221
+ showError(errorEl, 'Deletion failed');
251
222
  btn.disabled = false;
252
223
  btn.textContent = modal.dataset.deleteButtonLabel || 'Remove';
253
224
  });
@@ -281,10 +252,15 @@ if (!slackIntegrationInitialized) {
281
252
  div.className = 'slack-channel-item';
282
253
 
283
254
  const linkedLabel = modal.dataset.linkedLabel || '(linked)';
284
- div.innerHTML = `
285
- <strong>#${channel.name}</strong>
286
- ${isLinked ? `<span style="color:green;margin-left:0.5em;">${linkedLabel}</span>` : ''}
287
- `;
255
+ const nameStrong = document.createElement('strong');
256
+ nameStrong.textContent = `#${channel.name}`;
257
+ div.appendChild(nameStrong);
258
+ if (isLinked) {
259
+ const linkedSpan = document.createElement('span');
260
+ linkedSpan.style.cssText = 'color:green;margin-left:0.5em;';
261
+ linkedSpan.textContent = linkedLabel;
262
+ div.appendChild(linkedSpan);
263
+ }
288
264
 
289
265
  if (isSelected) {
290
266
  div.classList.add('active');
@@ -316,25 +292,20 @@ if (!slackIntegrationInitialized) {
316
292
 
317
293
  function performLink() {
318
294
  if (!creativeId || !selectedChannel) {
319
- showError(modal.dataset.noCreative);
295
+ showError(errorEl, modal.dataset.noCreative);
320
296
  return;
321
297
  }
322
298
 
323
299
  finishBtn.disabled = true;
324
300
  finishBtn.textContent = modal.dataset.linking || 'Linking...';
325
- clearError();
301
+ clearError(errorEl);
326
302
 
327
- fetch(`/slack/creatives/${creativeId}/slack_integrations`, {
303
+ fetchWithCsrf(`/slack/creatives/${creativeId}/slack_integrations`, {
328
304
  method: 'POST',
329
- headers: {
330
- 'X-CSRF-Token': csrfToken(),
331
- 'Content-Type': 'application/json',
332
- 'Accept': 'application/json'
333
- },
334
- body: JSON.stringify({
305
+ body: {
335
306
  channel_id: selectedChannel.id,
336
307
  channel_name: selectedChannel.name
337
- })
308
+ }
338
309
  })
339
310
  .then(response => response.json())
340
311
  .then(data => {
@@ -346,12 +317,12 @@ if (!slackIntegrationInitialized) {
346
317
  resetWizard();
347
318
  }, 2000);
348
319
  } else {
349
- showError(data.message || data.error || 'Failed to link channel');
320
+ showError(errorEl, data.message || data.error || 'Failed to link channel');
350
321
  }
351
322
  })
352
323
  .catch(error => {
353
324
  console.error('Link error:', error);
354
- showError('Failed to link channel');
325
+ showError(errorEl, 'Failed to link channel');
355
326
  })
356
327
  .finally(() => {
357
328
  finishBtn.disabled = false;
@@ -371,34 +342,20 @@ if (!slackIntegrationInitialized) {
371
342
  loadIntegrationStatus();
372
343
  });
373
344
 
374
- closeBtn.addEventListener('click', function () {
375
- modal.style.display = 'none';
376
- resetWizard();
377
- });
345
+ setupModalClose(modal, closeBtn, resetWizard);
378
346
 
379
347
  loginBtn.addEventListener('click', function (e) {
380
348
  e.preventDefault();
381
349
  console.log('Slack login button clicked');
382
- const width = parseInt(this.dataset.windowWidth) || 600;
383
- const height = parseInt(this.dataset.windowHeight) || 700;
384
- const left = (screen.width - width) / 2;
385
- const top = (screen.height - height) / 2;
386
-
387
- const authUrl = this.href;
388
- const authWindow = window.open(authUrl, 'slack-auth-window',
389
- `width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes`);
390
-
391
- if (authWindow) {
392
- console.log('Auth window opened');
393
-
394
- const checkClosed = setInterval(() => {
395
- if (authWindow.closed) {
396
- clearInterval(checkClosed);
397
- console.log('Auth window closed, reloading integration status');
398
- setTimeout(() => loadIntegrationStatus(), 1000);
399
- }
400
- }, 1000);
401
- }
350
+ openOAuthPopup('slack-auth-window', {
351
+ width: parseInt(this.dataset.windowWidth) || 600,
352
+ height: parseInt(this.dataset.windowHeight) || 700,
353
+ url: this.href,
354
+ onClose: function () {
355
+ console.log('Auth window closed, reloading integration status');
356
+ loadIntegrationStatus();
357
+ }
358
+ });
402
359
  });
403
360
 
404
361
  prevBtn.addEventListener('click', function () {
@@ -411,14 +368,14 @@ if (!slackIntegrationInitialized) {
411
368
  });
412
369
 
413
370
  nextBtn.addEventListener('click', function () {
414
- clearError();
371
+ clearError(errorEl);
415
372
  if (currentStep === 'connect') {
416
373
  currentStep = 'channels';
417
374
  if (channelSearchEl) channelSearchEl.value = '';
418
375
  renderChannelList();
419
376
  } else if (currentStep === 'channels') {
420
377
  if (!selectedChannel) {
421
- showError('Please select a channel');
378
+ showError(errorEl, 'Please select a channel');
422
379
  return;
423
380
  }
424
381
  updateSummary();
@@ -451,13 +408,7 @@ if (!slackIntegrationInitialized) {
451
408
  refreshBtn.textContent = modal.dataset.loading || 'Loading...';
452
409
  selectedChannel = null;
453
410
 
454
- fetch(`/slack/creatives/${creativeId}/slack_integrations`, {
455
- method: 'GET',
456
- headers: {
457
- 'X-CSRF-Token': csrfToken(),
458
- 'Accept': 'application/json'
459
- }
460
- })
411
+ fetchWithCsrf(`/slack/creatives/${creativeId}/slack_integrations`)
461
412
  .then(response => response.json())
462
413
  .then(data => {
463
414
  if (data.connected) {
@@ -468,7 +419,7 @@ if (!slackIntegrationInitialized) {
468
419
  })
469
420
  .catch(error => {
470
421
  console.error('Error refreshing channels:', error);
471
- showError(modal.dataset.refreshError || 'Failed to refresh channels');
422
+ showError(errorEl, modal.dataset.refreshError || 'Failed to refresh channels');
472
423
  })
473
424
  .finally(() => {
474
425
  refreshBtn.disabled = false;
@@ -478,12 +429,6 @@ if (!slackIntegrationInitialized) {
478
429
  });
479
430
  }
480
431
 
481
- modal.addEventListener('click', function (e) {
482
- if (e.target === modal) {
483
- modal.style.display = 'none';
484
- resetWizard();
485
- }
486
- });
487
432
  });
488
433
 
489
434
  // Slack badge for comments popup
@@ -1,24 +1,26 @@
1
1
  module CollavreSlack
2
2
  class SlackInboundMessageDeleteJob < ApplicationJob
3
+ include ErrorLoggable
4
+
3
5
  queue_as :default
4
6
 
5
7
  def perform(payload)
6
- data = payload.with_indifferent_access
7
- comment = Collavre::Comment.find_by(id: data[:comment_id])
8
+ with_slack_error_handling("SlackInboundMessageDeleteJob") do
9
+ data = payload.with_indifferent_access
10
+ comment = Collavre::Comment.find_by(id: data[:comment_id])
8
11
 
9
- if comment
10
- # Mark as coming from Slack to prevent loop
11
- comment.instance_variable_set(:@from_slack, true)
12
- comment.destroy!
13
- Rails.logger.info("[CollavreSlack] Deleted comment #{data[:comment_id]} from Slack deletion")
14
- end
12
+ if comment
13
+ # Mark as coming from Slack to prevent loop
14
+ comment.instance_variable_set(:@from_slack, true)
15
+ comment.destroy!
16
+ Rails.logger.info("[CollavreSlack] Deleted comment #{data[:comment_id]} from Slack deletion")
17
+ end
15
18
 
16
- # Clean up the comment link record
17
- if data[:slack_comment_link_id].present?
18
- SlackCommentLink.find_by(id: data[:slack_comment_link_id])&.destroy
19
+ # Clean up the comment link record
20
+ if data[:slack_comment_link_id].present?
21
+ SlackCommentLink.find_by(id: data[:slack_comment_link_id])&.destroy
22
+ end
19
23
  end
20
- rescue StandardError => e
21
- Rails.logger.error("[CollavreSlack] SlackInboundMessageDeleteJob error: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
22
24
  end
23
25
  end
24
26
  end
@@ -1,22 +1,24 @@
1
1
  module CollavreSlack
2
2
  class SlackInboundMessageUpdateJob < ApplicationJob
3
+ include ErrorLoggable
4
+
3
5
  queue_as :default
4
6
 
5
7
  def perform(payload)
6
- data = payload.with_indifferent_access
7
- comment = Collavre::Comment.find_by(id: data[:comment_id])
8
- return unless comment
8
+ with_slack_error_handling("SlackInboundMessageUpdateJob") do
9
+ data = payload.with_indifferent_access
10
+ comment = Collavre::Comment.find_by(id: data[:comment_id])
11
+ return unless comment
9
12
 
10
- new_content = data[:content]
11
- return if new_content.blank?
13
+ new_content = data[:content]
14
+ return if new_content.blank?
12
15
 
13
- # Mark as coming from Slack to prevent loop
14
- comment.instance_variable_set(:@from_slack, true)
15
- comment.update!(content: new_content)
16
+ # Mark as coming from Slack to prevent loop
17
+ comment.instance_variable_set(:@from_slack, true)
18
+ comment.update!(content: new_content)
16
19
 
17
- Rails.logger.info("[CollavreSlack] Updated comment #{comment.id} from Slack edit")
18
- rescue StandardError => e
19
- Rails.logger.error("[CollavreSlack] SlackInboundMessageUpdateJob error: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
20
+ Rails.logger.info("[CollavreSlack] Updated comment #{comment.id} from Slack edit")
21
+ end
20
22
  end
21
23
  end
22
24
  end
@@ -1,29 +1,31 @@
1
1
  module CollavreSlack
2
2
  class SlackMessageDeleteJob < ApplicationJob
3
+ include ErrorLoggable
4
+
3
5
  queue_as :default
4
6
 
5
7
  def perform(slack_channel_link_id:, message_ts:)
6
- Rails.logger.info("[CollavreSlack] SlackMessageDeleteJob performing for channel_link_id=#{slack_channel_link_id}, ts=#{message_ts}")
8
+ with_slack_error_handling("SlackMessageDeleteJob") do
9
+ Rails.logger.info("[CollavreSlack] SlackMessageDeleteJob performing for channel_link_id=#{slack_channel_link_id}, ts=#{message_ts}")
7
10
 
8
- channel_link = SlackChannelLink.find_by(id: slack_channel_link_id)
9
- return unless channel_link
11
+ channel_link = SlackChannelLink.find_by(id: slack_channel_link_id)
12
+ return unless channel_link
10
13
 
11
- client = SlackClient.new(access_token: channel_link.slack_account.access_token)
12
- response = client.delete_message(
13
- channel: channel_link.channel_id,
14
- timestamp: message_ts
15
- )
14
+ client = SlackClient.new(access_token: channel_link.slack_account.access_token)
15
+ response = client.delete_message(
16
+ channel: channel_link.channel_id,
17
+ timestamp: message_ts
18
+ )
16
19
 
17
- if response[:ok]
18
- Rails.logger.info("[CollavreSlack] Message deleted successfully, ts=#{message_ts}")
19
- else
20
- # message_not_found is not an error - message may have been deleted on Slack already
21
- unless response[:error] == "message_not_found"
22
- Rails.logger.error("[CollavreSlack] Failed to delete message: #{response[:error]}")
20
+ if response[:ok]
21
+ Rails.logger.info("[CollavreSlack] Message deleted successfully, ts=#{message_ts}")
22
+ else
23
+ # message_not_found is not an error - message may have been deleted on Slack already
24
+ unless response[:error] == "message_not_found"
25
+ Rails.logger.error("[CollavreSlack] Failed to delete message: #{response[:error]}")
26
+ end
23
27
  end
24
28
  end
25
- rescue StandardError => e
26
- Rails.logger.error("[CollavreSlack] SlackMessageDeleteJob error: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
27
29
  end
28
30
  end
29
31
  end
@@ -1,32 +1,34 @@
1
1
  module CollavreSlack
2
2
  class SlackMessageUpdateJob < ApplicationJob
3
+ include ErrorLoggable
4
+
3
5
  queue_as :default
4
6
 
5
7
  def perform(slack_comment_link_id:, message:)
6
- Rails.logger.info("[CollavreSlack] SlackMessageUpdateJob performing for link_id=#{slack_comment_link_id}")
8
+ with_slack_error_handling("SlackMessageUpdateJob") do
9
+ Rails.logger.info("[CollavreSlack] SlackMessageUpdateJob performing for link_id=#{slack_comment_link_id}")
7
10
 
8
- comment_link = SlackCommentLink.find_by(id: slack_comment_link_id)
9
- return unless comment_link
11
+ comment_link = SlackCommentLink.find_by(id: slack_comment_link_id)
12
+ return unless comment_link
10
13
 
11
- channel_link = comment_link.slack_channel_link
12
- return unless channel_link
14
+ channel_link = comment_link.slack_channel_link
15
+ return unless channel_link
13
16
 
14
- formatted_message = MentionMapping.to_slack(message, channel_link.slack_account)
17
+ formatted_message = MentionMapping.to_slack(message, channel_link.slack_account)
15
18
 
16
- client = SlackClient.new(access_token: channel_link.slack_account.access_token)
17
- response = client.update_message(
18
- channel: channel_link.channel_id,
19
- timestamp: comment_link.message_ts,
20
- text: formatted_message
21
- )
19
+ client = SlackClient.new(access_token: channel_link.slack_account.access_token)
20
+ response = client.update_message(
21
+ channel: channel_link.channel_id,
22
+ timestamp: comment_link.message_ts,
23
+ text: formatted_message
24
+ )
22
25
 
23
- if response[:ok]
24
- Rails.logger.info("[CollavreSlack] Message updated successfully, ts=#{comment_link.message_ts}")
25
- else
26
- Rails.logger.error("[CollavreSlack] Failed to update message: #{response[:error]}")
26
+ if response[:ok]
27
+ Rails.logger.info("[CollavreSlack] Message updated successfully, ts=#{comment_link.message_ts}")
28
+ else
29
+ Rails.logger.error("[CollavreSlack] Failed to update message: #{response[:error]}")
30
+ end
27
31
  end
28
- rescue StandardError => e
29
- Rails.logger.error("[CollavreSlack] SlackMessageUpdateJob error: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
30
32
  end
31
33
  end
32
34
  end
@@ -0,0 +1,20 @@
1
+ module CollavreSlack
2
+ module ErrorLoggable
3
+ extend ActiveSupport::Concern
4
+
5
+ private
6
+
7
+ # Wraps a block with standardized Slack error handling.
8
+ # Logs the error message and first 5 backtrace lines.
9
+ #
10
+ # @param context_name [String] identifies the caller in the log (e.g. "SlackMessageJob")
11
+ # @param reraise [Boolean] whether to re-raise after logging (default: false)
12
+ # @yield the block to execute with error handling
13
+ def with_slack_error_handling(context_name, reraise: false)
14
+ yield
15
+ rescue StandardError => e
16
+ Rails.logger.error("[CollavreSlack] #{context_name} error: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
17
+ raise if reraise
18
+ end
19
+ end
20
+ end
@@ -3,6 +3,8 @@ module CollavreSlack
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
+ include ErrorLoggable
7
+
6
8
  after_create_commit :dispatch_to_slack_channels
7
9
  after_update_commit :update_slack_messages, if: :saved_change_to_content?
8
10
  before_destroy :prepare_slack_message_deletion
@@ -12,61 +14,61 @@ module CollavreSlack
12
14
  private
13
15
 
14
16
  def dispatch_to_slack_channels
15
- # Don't dispatch if this comment came from Slack (prevent loop)
16
- if instance_variable_get(:@from_slack)
17
- Rails.logger.info("[CollavreSlack] Skipping dispatch - comment came from Slack")
18
- return
19
- end
17
+ with_slack_error_handling("Failed to dispatch to Slack") do
18
+ # Don't dispatch if this comment came from Slack (prevent loop)
19
+ if instance_variable_get(:@from_slack)
20
+ Rails.logger.info("[CollavreSlack] Skipping dispatch - comment came from Slack")
21
+ return
22
+ end
20
23
 
21
- # Use effective_origin since Slack links are on origin creative
22
- target_creative_id = creative.effective_origin.id
24
+ # Use effective_origin since Slack links are on origin creative
25
+ target_creative_id = creative.effective_origin.id
23
26
 
24
- return unless CollavreSlack::SlackChannelLink.where(creative_id: target_creative_id).exists?
27
+ return unless CollavreSlack::SlackChannelLink.where(creative_id: target_creative_id).exists?
25
28
 
26
- Rails.logger.info("[CollavreSlack] dispatch_to_slack_channels called for comment #{id}, creative_id=#{creative_id}, target_creative_id=#{target_creative_id}")
29
+ Rails.logger.info("[CollavreSlack] dispatch_to_slack_channels called for comment #{id}, creative_id=#{creative_id}, target_creative_id=#{target_creative_id}")
27
30
 
28
- links = CollavreSlack::SlackChannelLink.where(creative_id: target_creative_id)
29
- Rails.logger.info("[CollavreSlack] Found #{links.count} active Slack links")
31
+ links = CollavreSlack::SlackChannelLink.where(creative_id: target_creative_id)
32
+ Rails.logger.info("[CollavreSlack] Found #{links.count} active Slack links")
30
33
 
31
- links.find_each do |link|
32
- Rails.logger.info("[CollavreSlack] Dispatching to channel #{link.channel_name} (#{link.channel_id})")
33
- dispatcher = CollavreSlack::SlackMessageDispatcher.new(channel_link: link)
34
- dispatcher.enqueue(message: format_slack_message, sender: user, comment: self)
34
+ links.find_each do |link|
35
+ Rails.logger.info("[CollavreSlack] Dispatching to channel #{link.channel_name} (#{link.channel_id})")
36
+ dispatcher = CollavreSlack::SlackMessageDispatcher.new(channel_link: link)
37
+ dispatcher.enqueue(message: format_slack_message, sender: user, comment: self)
38
+ end
35
39
  end
36
- rescue StandardError => e
37
- Rails.logger.error("[CollavreSlack] Failed to dispatch to Slack: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
38
40
  end
39
41
 
40
42
  def update_slack_messages
41
- return if instance_variable_get(:@from_slack)
42
-
43
- comment_links = CollavreSlack::SlackCommentLink.where(comment_id: id)
44
- return if comment_links.empty?
45
-
46
- comment_links.each do |link|
47
- CollavreSlack::SlackMessageUpdateJob.perform_later(
48
- slack_comment_link_id: link.id,
49
- message: format_slack_message
50
- )
43
+ with_slack_error_handling("Failed to update Slack messages") do
44
+ return if instance_variable_get(:@from_slack)
45
+
46
+ comment_links = CollavreSlack::SlackCommentLink.where(comment_id: id)
47
+ return if comment_links.empty?
48
+
49
+ comment_links.each do |link|
50
+ CollavreSlack::SlackMessageUpdateJob.perform_later(
51
+ slack_comment_link_id: link.id,
52
+ message: format_slack_message
53
+ )
54
+ end
51
55
  end
52
- rescue StandardError => e
53
- Rails.logger.error("[CollavreSlack] Failed to update Slack messages: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
54
56
  end
55
57
 
56
58
  def prepare_slack_message_deletion
57
- Rails.logger.info("[CollavreSlack] prepare_slack_message_deletion called for comment #{id}")
58
- return if instance_variable_get(:@from_slack)
59
-
60
- # Save link info before the comment is deleted
61
- # The database will cascade delete the records, but we need the info for Slack API
62
- comment_links = CollavreSlack::SlackCommentLink.where(comment_id: id)
63
- Rails.logger.info("[CollavreSlack] Found #{comment_links.count} slack comment links")
64
- @slack_links_to_delete = comment_links.map do |link|
65
- { slack_channel_link_id: link.slack_channel_link_id, message_ts: link.message_ts }
59
+ with_slack_error_handling("Failed to prepare Slack message deletion") do
60
+ Rails.logger.info("[CollavreSlack] prepare_slack_message_deletion called for comment #{id}")
61
+ return if instance_variable_get(:@from_slack)
62
+
63
+ # Save link info before the comment is deleted
64
+ # The database will cascade delete the records, but we need the info for Slack API
65
+ comment_links = CollavreSlack::SlackCommentLink.where(comment_id: id)
66
+ Rails.logger.info("[CollavreSlack] Found #{comment_links.count} slack comment links")
67
+ @slack_links_to_delete = comment_links.map do |link|
68
+ { slack_channel_link_id: link.slack_channel_link_id, message_ts: link.message_ts }
69
+ end
70
+ Rails.logger.info("[CollavreSlack] Saved #{@slack_links_to_delete.size} links for deletion")
66
71
  end
67
- Rails.logger.info("[CollavreSlack] Saved #{@slack_links_to_delete.size} links for deletion")
68
- rescue StandardError => e
69
- Rails.logger.error("[CollavreSlack] Failed to prepare Slack message deletion: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
70
72
  end
71
73
 
72
74
  def format_slack_message
@@ -76,22 +78,22 @@ module CollavreSlack
76
78
  end
77
79
 
78
80
  def delete_slack_messages
79
- Rails.logger.info("[CollavreSlack] delete_slack_messages called")
80
- return if instance_variable_get(:@from_slack)
81
-
82
- links_to_delete = instance_variable_get(:@slack_links_to_delete) || []
83
- Rails.logger.info("[CollavreSlack] Links to delete: #{links_to_delete.size}")
84
- return if links_to_delete.empty?
85
-
86
- links_to_delete.each do |link_info|
87
- Rails.logger.info("[CollavreSlack] Enqueuing SlackMessageDeleteJob for ts=#{link_info[:message_ts]}")
88
- CollavreSlack::SlackMessageDeleteJob.perform_later(
89
- slack_channel_link_id: link_info[:slack_channel_link_id],
90
- message_ts: link_info[:message_ts]
91
- )
81
+ with_slack_error_handling("Failed to delete Slack messages") do
82
+ Rails.logger.info("[CollavreSlack] delete_slack_messages called")
83
+ return if instance_variable_get(:@from_slack)
84
+
85
+ links_to_delete = instance_variable_get(:@slack_links_to_delete) || []
86
+ Rails.logger.info("[CollavreSlack] Links to delete: #{links_to_delete.size}")
87
+ return if links_to_delete.empty?
88
+
89
+ links_to_delete.each do |link_info|
90
+ Rails.logger.info("[CollavreSlack] Enqueuing SlackMessageDeleteJob for ts=#{link_info[:message_ts]}")
91
+ CollavreSlack::SlackMessageDeleteJob.perform_later(
92
+ slack_channel_link_id: link_info[:slack_channel_link_id],
93
+ message_ts: link_info[:message_ts]
94
+ )
95
+ end
92
96
  end
93
- rescue StandardError => e
94
- Rails.logger.error("[CollavreSlack] Failed to delete Slack messages: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
95
97
  end
96
98
  end
97
99
  end
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head><title><%= t("collavre_slack.views.auth.success_title") %></title></head>
4
+ <body>
5
+ <p><%= t("collavre_slack.views.auth.success_message") %></p>
6
+ <script>
7
+ if (window.opener) {
8
+ window.opener.postMessage({ type: 'slack-auth-success' }, window.location.origin);
9
+ }
10
+ window.close();
11
+ </script>
12
+ </body>
13
+ </html>
@@ -1,3 +1,3 @@
1
1
  module CollavreSlack
2
- VERSION = "0.2.4"
2
+ VERSION = "0.2.6"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: collavre_slack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Collavre
@@ -100,6 +100,7 @@ files:
100
100
  - app/models/collavre_slack/slack_comment_link.rb
101
101
  - app/models/collavre_slack/slack_message_log.rb
102
102
  - app/models/collavre_slack/slack_user_mapping.rb
103
+ - app/models/concerns/collavre_slack/error_loggable.rb
103
104
  - app/models/concerns/collavre_slack/slack_dispatchable.rb
104
105
  - app/models/concerns/collavre_slack/slack_reaction_dispatchable.rb
105
106
  - app/services/collavre_slack/emoji_mapping.rb
@@ -110,6 +111,7 @@ files:
110
111
  - app/services/collavre_slack/slack_message_dispatcher.rb
111
112
  - app/views/collavre_slack/creatives/slack_integrations/index.html.erb
112
113
  - app/views/collavre_slack/integrations/_modal.html.erb
114
+ - app/views/collavre_slack/slack_auth/callback_success.html.erb
113
115
  - config/initializers/collavre_slack.rb
114
116
  - config/locales/en.yml
115
117
  - config/locales/ko.yml