feat: Slack link unfurling (#7940)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Muhsin Keloth
2023-09-29 19:35:56 +05:30
committed by GitHub
parent 845b0c0a68
commit 24fe3805d8
16 changed files with 525 additions and 15 deletions

View File

@@ -0,0 +1,38 @@
require 'rails_helper'
RSpec.describe SlackUnfurlJob do
subject(:job) { described_class.perform_later(params: link_shared, integration_hook: hook) }
let(:account) { create(:account) }
let(:hook) { create(:integrations_hook, account: account) }
let(:inbox) { create(:inbox, account: account) }
let!(:conversation) { create(:conversation, inbox: inbox) }
let(:link_shared) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge(links: [{
url: "https://qa.chatwoot.com/app/accounts/1/conversations/#{conversation.display_id}",
domain: 'qa.chatwoot.com'
}]),
type: 'event_callback',
event_time: 1_588_623_033
}
end
it 'enqueues the job' do
expect { job }.to have_enqueued_job(described_class)
.on_queue('low')
end
it 'calls the SlackLinkUnfurlService' do
slack_link_unfurl_service = instance_double(Integrations::Slack::SlackLinkUnfurlService)
expect(Integrations::Slack::SlackLinkUnfurlService).to receive(:new)
.with(params: link_shared, integration_hook: hook)
.and_return(slack_link_unfurl_service)
expect(slack_link_unfurl_service).to receive(:perform)
described_class.perform_now(link_shared, hook)
end
end

View File

@@ -3,6 +3,7 @@ require 'rails_helper'
describe Integrations::Slack::IncomingMessageBuilder do
let(:account) { create(:account) }
let(:message_params) { slack_message_stub }
let(:builder) { described_class.new(hook: hook) }
let(:private_message_params) do
{
team_id: 'TLST3048H',
@@ -30,6 +31,8 @@ describe Integrations::Slack::IncomingMessageBuilder do
event_time: 1_588_623_033
}
end
let(:slack_client) { double }
let(:link_unfurl_service) { double }
let(:message_with_attachments) { slack_attachment_stub }
let(:message_without_thread_ts) { slack_message_stub_without_thread_ts }
let(:verification_params) { slack_url_verification_stub }
@@ -145,5 +148,24 @@ describe Integrations::Slack::IncomingMessageBuilder do
expect(conversation.messages.count).to eql(messages_count)
end
end
context 'when link shared' do
let(:link_shared) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge({ links: [{ url: "https://qa.chatwoot.com/app/accounts/1/conversations/#{conversation.display_id}",
domain: 'qa.chatwoot.com' }] }),
type: 'event_callback',
event_time: 1_588_623_033
}
end
it 'unfurls link' do
builder = described_class.new(link_shared)
expect(SlackUnfurlJob).to receive(:perform_later).with(link_shared, hook)
builder.perform
end
end
end
end

View File

@@ -0,0 +1,61 @@
require 'rails_helper'
describe Integrations::Slack::LinkUnfurlFormatter do
let(:contact) { create(:contact) }
let(:inbox) { create(:inbox) }
let(:url) { 'https://example.com/app/accounts/1/conversations/100' }
describe '#perform' do
context 'when unfurling a URL' do
let(:user_info) do
{
user_name: contact.name,
email: contact.email,
phone_number: '---',
company_name: '---'
}
end
let(:expected_payload) do
{
url => {
'blocks' => [
{
'type' => 'section',
'fields' => [
{ 'type' => 'mrkdwn', 'text' => "*Name:*\n#{contact.name}" },
{ 'type' => 'mrkdwn', 'text' => "*Email:*\n#{contact.email}" },
{ 'type' => 'mrkdwn', 'text' => "*Phone:*\n---" },
{ 'type' => 'mrkdwn', 'text' => "*Company:*\n---" },
{ 'type' => 'mrkdwn', 'text' => "*Inbox:*\n#{inbox.name}" },
{ 'type' => 'mrkdwn', 'text' => "*Inbox Type:*\n#{inbox.channel_type}" }
]
},
{
'type' => 'actions',
'elements' => [
{
'type' => 'button',
'text' => { 'type' => 'plain_text', 'text' => 'Open conversation', 'emoji' => true },
'url' => url,
'action_id' => 'button-action'
}
]
}
]
}
}
end
it 'returns expected unfurl blocks when the URL is not blank' do
formatter = described_class.new(url: url, user_info: user_info, inbox_name: inbox.name, inbox_type: inbox.channel_type)
expect(formatter.perform).to eq(expected_payload)
end
it 'returns an empty hash when the URL is blank' do
formatter = described_class.new(url: nil, user_info: user_info, inbox_name: inbox.name, inbox_type: inbox.channel_type)
expect(formatter.perform).to eq({})
end
end
end
end

View File

@@ -18,12 +18,14 @@ describe Integrations::Slack::SendOnSlackService do
let(:slack_message_content) { double }
let(:slack_client) { double }
let(:builder) { described_class.new(message: message, hook: hook) }
let(:link_builder) { described_class.new(message: nil, hook: hook) }
let(:conversation_link) do
"<#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}|Click here> to view the conversation."
end
before do
allow(builder).to receive(:slack_client).and_return(slack_client)
allow(link_builder).to receive(:slack_client).and_return(slack_client)
allow(slack_message).to receive(:[]).with('ts').and_return('12345.6789')
allow(slack_message).to receive(:[]).with('message').and_return(slack_message_content)
allow(slack_message_content).to receive(:[]).with('ts').and_return('6789.12345')
@@ -135,6 +137,18 @@ describe Integrations::Slack::SendOnSlackService do
expect(conversation.identifier).to eq '1691652432.896169'
end
it 'sent lnk unfurl to slack' do
unflur_payload = { :channel => 'channel',
:ts => 'timestamp',
:unfurls =>
{ :'https://qa.chatwoot.com/app/accounts/1/conversations/1' =>
{ :blocks => [{ :type => 'section',
:text => { :type => 'plain_text', :text => 'This is a plain text section block.', :emoji => true } }] } } }
allow(slack_client).to receive(:chat_unfurl).with(unflur_payload)
link_builder.link_unfurl(unflur_payload)
expect(slack_client).to have_received(:chat_unfurl).with(unflur_payload)
end
it 'sent attachment on slack' do
expect(slack_client).to receive(:chat_postMessage).with(
channel: hook.reference_id,

View File

@@ -0,0 +1,163 @@
require 'rails_helper'
describe Integrations::Slack::SlackLinkUnfurlService do
let!(:contact) { create(:contact, name: 'Contact 1', email: nil, phone_number: nil) }
let(:channel_email) { create(:channel_email) }
let!(:conversation) { create(:conversation, inbox: channel_email.inbox, contact: contact, identifier: nil) }
let(:account) { conversation.account }
let!(:hook) { create(:integrations_hook, account: account) }
describe '#perform' do
context 'when the event does not contain any link' do
let(:link_shared) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge(links: []),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:link_builder) { described_class.new(params: link_shared, integration_hook: hook) }
it 'does not send a POST request to Slack API' do
result = link_builder.perform
expect(result).to eq([])
end
end
context 'when the event link contains the account id which does not match the integration hook account id' do
let(:link_shared) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge(links: [{
url: "https://qa.chatwoot.com/app/accounts/1212/conversations/#{conversation.display_id}",
domain: 'qa.chatwoot.com'
}], channel: 'G054F6A6Q'),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:link_builder) { described_class.new(params: link_shared, integration_hook: hook) }
it 'does not send a POST request to Slack API' do
link_builder.perform
expect(link_builder).not_to receive(:unfurl_link)
end
end
context 'when the event link contains the conversation id which does not belong to the account' do
let(:link_shared) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge(links: [{
url: 'https://qa.chatwoot.com/app/accounts/1/conversations/1213',
domain: 'qa.chatwoot.com'
}], channel: 'G054F6A6Q'),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:link_builder) { described_class.new(params: link_shared, integration_hook: hook) }
it 'does not send a POST request to Slack API' do
link_builder.perform
expect(link_builder).not_to receive(:unfurl_link)
end
end
context 'when the event contains containing single link' do
let(:link_shared) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge(links: [{
url: "https://qa.chatwoot.com/app/accounts/1/conversations/#{conversation.display_id}",
domain: 'qa.chatwoot.com'
}]),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:link_builder) { described_class.new(params: link_shared, integration_hook: hook) }
it 'sends a POST unfurl request to Slack' do
expected_body = {
'source' => 'conversations_history',
'unfurl_id' => 'C7NQEAE5Q.1695111587.937099.7e240338c6d2053fb49f56808e7c1f619f6ef317c39ebc59fc4af1cc30dce49b',
'unfurls' => '{"https://qa.chatwoot.com/app/accounts/1/conversations/1":' \
'{"blocks":[{' \
'"type":"section",' \
'"fields":[{' \
'"type":"mrkdwn",' \
"\"text\":\"*Name:*\\n#{contact.name}\"}," \
'{"type":"mrkdwn","text":"*Email:*\\n---"},' \
'{"type":"mrkdwn","text":"*Phone:*\\n---"},' \
'{"type":"mrkdwn","text":"*Company:*\\n---"},' \
"{\"type\":\"mrkdwn\",\"text\":\"*Inbox:*\\n#{channel_email.inbox.name}\"}," \
"{\"type\":\"mrkdwn\",\"text\":\"*Inbox Type:*\\n#{channel_email.inbox.channel.name}\"}]}," \
'{"type":"actions","elements":[{' \
'"type":"button",' \
'"text":{"type":"plain_text","text":"Open conversation","emoji":true},' \
'"url":"https://qa.chatwoot.com/app/accounts/1/conversations/1",' \
'"action_id":"button-action"}]}]}}'
}
stub_request(:post, 'https://slack.com/api/chat.unfurl')
.with(body: expected_body)
.to_return(status: 200, body: '', headers: {})
result = link_builder.perform
expect(result).to eq([{ url: 'https://qa.chatwoot.com/app/accounts/1/conversations/1', domain: 'qa.chatwoot.com' }])
end
end
context 'when the event contains containing multiple links' do
let(:link_shared_1) do
{
team_id: 'TLST3048H',
api_app_id: 'A012S5UETV4',
event: link_shared_event.merge(links: [{
url: "https://qa.chatwoot.com/app/accounts/1/conversations/#{conversation.display_id}",
domain: 'qa.chatwoot.com'
},
{
url: "https://qa.chatwoot.com/app/accounts/1/conversations/#{conversation.display_id}",
domain: 'qa.chatwoot.com'
}]),
type: 'event_callback',
event_time: 1_588_623_033
}
end
let(:link_builder) { described_class.new(params: link_shared_1, integration_hook: hook) }
it('sends multiple POST unfurl request to Slack') do
expected_body = {
'source' => 'conversations_history',
'unfurl_id' => 'C7NQEAE5Q.1695111587.937099.7e240338c6d2053fb49f56808e7c1f619f6ef317c39ebc59fc4af1cc30dce49b',
'unfurls' => '{"https://qa.chatwoot.com/app/accounts/1/conversations/1":' \
'{"blocks":[{' \
'"type":"section",' \
'"fields":[{' \
'"type":"mrkdwn",' \
"\"text\":\"*Name:*\\n#{contact.name}\"}," \
'{"type":"mrkdwn","text":"*Email:*\\n---"},' \
'{"type":"mrkdwn","text":"*Phone:*\\n---"},' \
'{"type":"mrkdwn","text":"*Company:*\\n---"},' \
"{\"type\":\"mrkdwn\",\"text\":\"*Inbox:*\\n#{channel_email.inbox.name}\"}," \
"{\"type\":\"mrkdwn\",\"text\":\"*Inbox Type:*\\n#{channel_email.inbox.channel.name}\"}]}," \
'{"type":"actions","elements":[{' \
'"type":"button",' \
'"text":{"type":"plain_text","text":"Open conversation","emoji":true},' \
'"url":"https://qa.chatwoot.com/app/accounts/1/conversations/1",' \
'"action_id":"button-action"}]}]}}'
}
stub_request(:post, 'https://slack.com/api/chat.unfurl')
.with(body: expected_body)
.to_return(status: 200, body: '', headers: {})
expect { link_builder.perform }.not_to raise_error
end
end
end
end

View File

@@ -107,4 +107,14 @@ module SlackStubs
channel: 'G01354F6A6Q'
}
end
def link_shared_event
{
type: 'link_shared',
user: 'ULYPAKE5S',
source: 'conversations_history',
unfurl_id: 'C7NQEAE5Q.1695111587.937099.7e240338c6d2053fb49f56808e7c1f619f6ef317c39ebc59fc4af1cc30dce49b',
channel: 'G01354F6A6Q'
}
end
end