Feature: Support account/inbox specific webhooks (#562)

This commit is contained in:
Subin T P
2020-02-26 09:44:24 +05:30
committed by GitHub
parent 4d5e7b4adf
commit 7479b5db43
24 changed files with 117 additions and 57 deletions

View File

@@ -1,4 +1,4 @@
class Api::V1::Inbox::WebhooksController < Api::BaseController class Api::V1::Account::WebhooksController < Api::BaseController
before_action :check_authorization before_action :check_authorization
before_action :fetch_webhook, only: [:update, :destroy] before_action :fetch_webhook, only: [:update, :destroy]
@@ -23,7 +23,7 @@ class Api::V1::Inbox::WebhooksController < Api::BaseController
private private
def webhook_params def webhook_params
params.require(:webhook).permit(:account_id, :inbox_id, :urls).merge(urls: params[:urls]) params.require(:webhook).permit(:account_id, :inbox_id, :url)
end end
def fetch_webhook def fetch_webhook

View File

@@ -3,13 +3,17 @@ class WebhookListener < BaseListener
message = extract_message_and_account(event)[0] message = extract_message_and_account(event)[0]
inbox = message.inbox inbox = message.inbox
return unless message.reportable? && inbox.webhook.present? return unless message.reportable?
webhook = message.inbox.webhook payload = message.webhook_data.merge(event: __method__.to_s)
payload = message.push_event_data.merge(event: __method__.to_s) # Account webhooks
inbox.account.webhooks.account.each do |webhook|
WebhookJob.perform_later(webhook.url, payload)
end
webhook.urls.each do |url| # Inbox webhooks
WebhookJob.perform_later(url, payload) inbox.webhooks.inbox.each do |webhook|
WebhookJob.perform_later(webhook.url, payload)
end end
end end
end end

View File

@@ -60,6 +60,13 @@ class Account < ApplicationRecord
} }
end end
def webhook_data
{
id: id,
name: name
}
end
private private
def create_subscription def create_subscription

View File

@@ -41,4 +41,11 @@ class Contact < ApplicationRecord
pubsub_token: pubsub_token pubsub_token: pubsub_token
} }
end end
def webhook_data
{
id: id,
name: name
}
end
end end

View File

@@ -93,6 +93,13 @@ class Conversation < ApplicationRecord
Conversations::EventDataPresenter.new(self).lock_data Conversations::EventDataPresenter.new(self).lock_data
end end
def webhook_data
{
display_id: display_id,
additional_attributes: additional_attributes
}
end
private private
def dispatch_events def dispatch_events

View File

@@ -33,7 +33,7 @@ class Inbox < ApplicationRecord
has_many :members, through: :inbox_members, source: :user has_many :members, through: :inbox_members, source: :user
has_many :conversations, dependent: :destroy has_many :conversations, dependent: :destroy
has_many :messages, through: :conversations has_many :messages, through: :conversations
has_one :webhook, dependent: :destroy has_many :webhooks, dependent: :destroy
after_create :subscribe_webhook, if: :facebook? after_create :subscribe_webhook, if: :facebook?
after_destroy :delete_round_robin_agents after_destroy :delete_round_robin_agents
@@ -60,6 +60,13 @@ class Inbox < ApplicationRecord
account.users.find_by(id: user_id) account.users.find_by(id: user_id)
end end
def webhook_data
{
id: id,
name: name
}
end
private private
def delete_round_robin_agents def delete_round_robin_agents

View File

@@ -50,6 +50,7 @@ class Message < ApplicationRecord
belongs_to :inbox belongs_to :inbox
belongs_to :conversation belongs_to :conversation
belongs_to :user, required: false belongs_to :user, required: false
belongs_to :contact, required: false
has_one :attachment, dependent: :destroy, autosave: true has_one :attachment, dependent: :destroy, autosave: true
@@ -78,6 +79,21 @@ class Message < ApplicationRecord
incoming? || outgoing? incoming? || outgoing?
end end
def webhook_data
{
id: id,
content: content,
created_at: created_at,
message_type: message_type,
source_id: source_id,
sender: user.try(:webhook_data),
contact: contact.try(:webhook_data),
inbox: inbox.webhook_data,
conversation: conversation.webhook_data,
account: account.webhook_data
}
end
private private
def dispatch_event def dispatch_event

View File

@@ -110,4 +110,12 @@ class User < ApplicationRecord
avatar_url: avatar_url avatar_url: avatar_url
} }
end end
def webhook_data
{
id: id,
name: name,
email: email
}
end
end end

View File

@@ -2,19 +2,20 @@
# #
# Table name: webhooks # Table name: webhooks
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# urls :string # url :string
# created_at :datetime not null # webhook_type :integer default("account")
# updated_at :datetime not null # created_at :datetime not null
# account_id :integer # updated_at :datetime not null
# inbox_id :integer # account_id :integer
# inbox_id :integer
# #
class Webhook < ApplicationRecord class Webhook < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :inbox belongs_to :inbox, optional: true
validates :account_id, presence: true validates :account_id, presence: true
validates :inbox_id, presence: true
serialize :urls, Array enum webhook_type: { account: 0, inbox: 1 }
end end

View File

@@ -0,0 +1,9 @@
json.id webhook.id
json.url webhook.url
json.account_id webhook.account_id
if webhook.inbox
json.inbox do
json.id webhook.inbox.id
json.name webhook.inbox.name
end
end

View File

@@ -1,7 +0,0 @@
json.id webhook.id
json.urls webhook.urls
json.account_id webhook.account_id
json.inbox do
json.id webhook.inbox.id
json.name webhook.inbox.name
end

View File

@@ -38,7 +38,7 @@ Rails.application.routes.draw do
resource :contact_merge, only: [:create] resource :contact_merge, only: [:create]
end end
namespace :inbox do namespace :account do
resources :webhooks, except: [:show] resources :webhooks, except: [:show]
end end

View File

@@ -0,0 +1,5 @@
class RenameUrlsToUrl < ActiveRecord::Migration[6.0]
def change
rename_column :webhooks, :urls, :url
end
end

View File

@@ -0,0 +1,5 @@
class AddTypeToWebhook < ActiveRecord::Migration[6.0]
def change
add_column :webhooks, :webhook_type, :integer, default: '0'
end
end

View File

@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_02_17_192734) do ActiveRecord::Schema.define(version: 2020_02_25_162150) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -262,9 +262,10 @@ ActiveRecord::Schema.define(version: 2020_02_17_192734) do
create_table "webhooks", force: :cascade do |t| create_table "webhooks", force: :cascade do |t|
t.integer "account_id" t.integer "account_id"
t.integer "inbox_id" t.integer "inbox_id"
t.string "urls" t.string "url"
t.datetime "created_at", precision: 6, null: false t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
t.integer "webhook_type", default: 0
end end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"

View File

@@ -3,99 +3,91 @@ require 'rails_helper'
RSpec.describe 'Webhooks API', type: :request do RSpec.describe 'Webhooks API', type: :request do
let(:account) { create(:account) } let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) } let(:inbox) { create(:inbox, account: account) }
let(:webhook) { create(:webhook, account: account, inbox: inbox, urls: ['https://hello.com']) } let(:webhook) { create(:webhook, account: account, inbox: inbox, url: 'https://hello.com') }
let(:administrator) { create(:user, account: account, role: :administrator) } let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) } let(:agent) { create(:user, account: account, role: :agent) }
describe 'GET /api/v1/inbox/webhooks' do describe 'GET /api/v1/account/webhooks' do
context 'when it is an authenticated agent' do context 'when it is an authenticated agent' do
it 'returns unauthorized' do it 'returns unauthorized' do
get '/api/v1/inbox/webhooks', get '/api/v1/account/webhooks',
headers: agent.create_new_auth_token, headers: agent.create_new_auth_token,
as: :json as: :json
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
context 'when it is an authenticated admin user' do context 'when it is an authenticated admin user' do
it 'gets all webhook' do it 'gets all webhook' do
get '/api/v1/inbox/webhooks', get '/api/v1/account/webhooks',
headers: administrator.create_new_auth_token, headers: administrator.create_new_auth_token,
as: :json as: :json
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(JSON.parse(response.body)['payload']['webhooks'].count).to eql account.webhooks.count expect(JSON.parse(response.body)['payload']['webhooks'].count).to eql account.webhooks.count
end end
end end
end end
describe 'POST /api/v1/inbox/webhooks' do describe 'POST /api/v1/account/webhooks' do
context 'when it is an authenticated agent' do context 'when it is an authenticated agent' do
it 'returns unauthorized' do it 'returns unauthorized' do
post '/api/v1/inbox/webhooks', post '/api/v1/account/webhooks',
headers: agent.create_new_auth_token, headers: agent.create_new_auth_token,
as: :json as: :json
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
context 'when it is an authenticated admin user' do context 'when it is an authenticated admin user' do
it 'creates webhook' do it 'creates webhook' do
post '/api/v1/inbox/webhooks', post '/api/v1/account/webhooks',
params: { account_id: account.id, inbox_id: inbox.id, urls: ['https://hello.com'] }, params: { account_id: account.id, inbox_id: inbox.id, url: 'https://hello.com' },
headers: administrator.create_new_auth_token, headers: administrator.create_new_auth_token,
as: :json as: :json
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(JSON.parse(response.body)['payload']['webhook']['urls']).to eql ['https://hello.com'] expect(JSON.parse(response.body)['payload']['webhook']['url']).to eql 'https://hello.com'
end end
end end
end end
describe 'PUT /api/v1/inbox/webhooks/:id' do describe 'PUT /api/v1/account/webhooks/:id' do
context 'when it is an authenticated agent' do context 'when it is an authenticated agent' do
it 'returns unauthorized' do it 'returns unauthorized' do
put "/api/v1/inbox/webhooks/#{webhook.id}", put "/api/v1/account/webhooks/#{webhook.id}",
headers: agent.create_new_auth_token, headers: agent.create_new_auth_token,
as: :json as: :json
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
context 'when it is an authenticated admin user' do context 'when it is an authenticated admin user' do
it 'updates webhook' do it 'updates webhook' do
put "/api/v1/inbox/webhooks/#{webhook.id}", put "/api/v1/account/webhooks/#{webhook.id}",
params: { urls: ['https://hello.com', 'https://world.com'] }, params: { url: 'https://hello.com' },
headers: administrator.create_new_auth_token, headers: administrator.create_new_auth_token,
as: :json as: :json
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(JSON.parse(response.body)['payload']['webhook']['urls']).to eql ['https://hello.com', 'https://world.com'] expect(JSON.parse(response.body)['payload']['webhook']['url']).to eql 'https://hello.com'
end end
end end
end end
describe 'DELETE /api/v1/inbox/webhooks/:id' do describe 'DELETE /api/v1/account/webhooks/:id' do
context 'when it is an authenticated agent' do context 'when it is an authenticated agent' do
it 'returns unauthorized' do it 'returns unauthorized' do
delete "/api/v1/inbox/webhooks/#{webhook.id}", delete "/api/v1/account/webhooks/#{webhook.id}",
headers: agent.create_new_auth_token, headers: agent.create_new_auth_token,
as: :json as: :json
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
context 'when it is an authenticated admin user' do context 'when it is an authenticated admin user' do
it 'deletes webhook' do it 'deletes webhook' do
delete "/api/v1/inbox/webhooks/#{webhook.id}", delete "/api/v1/account/webhooks/#{webhook.id}",
headers: administrator.create_new_auth_token, headers: administrator.create_new_auth_token,
as: :json as: :json
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(account.webhooks.count).to be 0 expect(account.webhooks.count).to be 0
end end

View File

@@ -2,6 +2,6 @@ FactoryBot.define do
factory :webhook do factory :webhook do
account_id { 1 } account_id { 1 }
inbox_id { 1 } inbox_id { 1 }
urls { ['MyString'] } url { 'MyString' }
end end
end end

View File

@@ -25,7 +25,7 @@ describe WebhookListener do
context 'when webhook is configured' do context 'when webhook is configured' do
it 'triggers webhook' do it 'triggers webhook' do
webhook = create(:webhook, inbox: inbox, account: account) webhook = create(:webhook, inbox: inbox, account: account)
expect(WebhookJob).to receive(:perform_later).with(webhook.urls[0], message.push_event_data.merge(event: 'message_created')).once expect(WebhookJob).to receive(:perform_later).with(webhook.url, message.webhook_data.merge(event: 'message_created')).once
listener.message_created(event) listener.message_created(event)
end end
end end

View File

@@ -23,7 +23,7 @@ RSpec.describe Inbox do
it { is_expected.to have_many(:conversations).dependent(:destroy) } it { is_expected.to have_many(:conversations).dependent(:destroy) }
it { is_expected.to have_many(:messages).through(:conversations) } it { is_expected.to have_many(:messages).through(:conversations) }
it { is_expected.to have_one(:webhook) } it { is_expected.to have_many(:webhooks).dependent(:destroy) }
end end
describe '#add_member' do describe '#add_member' do

View File

@@ -3,11 +3,9 @@ require 'rails_helper'
RSpec.describe Webhook, type: :model do RSpec.describe Webhook, type: :model do
describe 'validations' do describe 'validations' do
it { is_expected.to validate_presence_of(:account_id) } it { is_expected.to validate_presence_of(:account_id) }
it { is_expected.to validate_presence_of(:inbox_id) }
end end
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:account) } it { is_expected.to belong_to(:account) }
it { is_expected.to belong_to(:inbox) }
end end
end end