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 +4 -4
- data/app/controllers/collavre_slack/creatives/slack_integrations_controller.rb +13 -18
- data/app/controllers/collavre_slack/slack_auth_controller.rb +1 -21
- data/app/javascript/collavre_slack.js +43 -98
- data/app/jobs/collavre_slack/slack_inbound_message_delete_job.rb +15 -13
- data/app/jobs/collavre_slack/slack_inbound_message_update_job.rb +13 -11
- data/app/jobs/collavre_slack/slack_message_delete_job.rb +18 -16
- data/app/jobs/collavre_slack/slack_message_update_job.rb +20 -18
- data/app/models/concerns/collavre_slack/error_loggable.rb +20 -0
- data/app/models/concerns/collavre_slack/slack_dispatchable.rb +58 -56
- data/app/views/collavre_slack/slack_auth/callback_success.html.erb +13 -0
- data/lib/collavre_slack/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8d470cccf0cf90daff6918a44ff263700803a3a51114003be481d51fc38431f9
|
|
4
|
+
data.tar.gz: 63c5cbebe3fd89218f494a41530e190d1ba85ca419e1baccc45914a40c6995aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
95
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
303
|
+
fetchWithCsrf(`/slack/creatives/${creativeId}/slack_integrations`, {
|
|
328
304
|
method: 'POST',
|
|
329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
13
|
+
new_content = data[:content]
|
|
14
|
+
return if new_content.blank?
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
11
|
+
channel_link = SlackChannelLink.find_by(id: slack_channel_link_id)
|
|
12
|
+
return unless channel_link
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
8
|
+
with_slack_error_handling("SlackMessageUpdateJob") do
|
|
9
|
+
Rails.logger.info("[CollavreSlack] SlackMessageUpdateJob performing for link_id=#{slack_comment_link_id}")
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
comment_link = SlackCommentLink.find_by(id: slack_comment_link_id)
|
|
12
|
+
return unless comment_link
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
channel_link = comment_link.slack_channel_link
|
|
15
|
+
return unless channel_link
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
formatted_message = MentionMapping.to_slack(message, channel_link.slack_account)
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
24
|
+
# Use effective_origin since Slack links are on origin creative
|
|
25
|
+
target_creative_id = creative.effective_origin.id
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
return unless CollavreSlack::SlackChannelLink.where(creative_id: target_creative_id).exists?
|
|
25
28
|
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
31
|
+
links = CollavreSlack::SlackChannelLink.where(creative_id: target_creative_id)
|
|
32
|
+
Rails.logger.info("[CollavreSlack] Found #{links.count} active Slack links")
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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>
|
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
|
+
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
|