Feature: Webhooks (#489)
This commit is contained in:
parent
79a847aeab
commit
919261d843
29 changed files with 566 additions and 214 deletions
|
@ -25,6 +25,7 @@ Metrics/BlockLength:
|
||||||
- spec/**/*
|
- spec/**/*
|
||||||
- '**/routes.rb'
|
- '**/routes.rb'
|
||||||
- 'config/environments/*'
|
- 'config/environments/*'
|
||||||
|
- db/schema.rb
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
- 'app/controllers/api/v1/widget/messages_controller.rb'
|
||||||
|
@ -38,6 +39,8 @@ Style/ClassAndModuleChildren:
|
||||||
RSpec/NestedGroups:
|
RSpec/NestedGroups:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Max: 4
|
Max: 4
|
||||||
|
RSpec/MessageSpies:
|
||||||
|
Enabled: false
|
||||||
AllCops:
|
AllCops:
|
||||||
Exclude:
|
Exclude:
|
||||||
- db/*
|
- db/*
|
||||||
|
|
1
Gemfile
1
Gemfile
|
@ -16,6 +16,7 @@ gem 'hashie'
|
||||||
gem 'jbuilder'
|
gem 'jbuilder'
|
||||||
gem 'kaminari'
|
gem 'kaminari'
|
||||||
gem 'responders'
|
gem 'responders'
|
||||||
|
gem 'rest-client'
|
||||||
gem 'time_diff'
|
gem 'time_diff'
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
gem 'valid_email2'
|
gem 'valid_email2'
|
||||||
|
|
|
@ -530,6 +530,7 @@ DEPENDENCIES
|
||||||
redis-namespace
|
redis-namespace
|
||||||
redis-rack-cache
|
redis-rack-cache
|
||||||
responders
|
responders
|
||||||
|
rest-client
|
||||||
rspec-rails (~> 4.0.0.beta2)
|
rspec-rails (~> 4.0.0.beta2)
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
|
|
36
app/controllers/api/v1/inbox/webhooks_controller.rb
Normal file
36
app/controllers/api/v1/inbox/webhooks_controller.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
class Api::V1::Inbox::WebhooksController < Api::BaseController
|
||||||
|
before_action :check_authorization
|
||||||
|
before_action :fetch_webhook, only: [:update, :destroy]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@webhooks = current_account.webhooks
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@webhook = current_account.webhooks.new(webhook_params)
|
||||||
|
@webhook.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@webhook.update!(webhook_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@webhook.destroy
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def webhook_params
|
||||||
|
params.require(:webhook).permit(:account_id, :inbox_id, :urls).merge(urls: params[:urls])
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_webhook
|
||||||
|
@webhook = current_account.webhooks.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_authorization
|
||||||
|
authorize(Webhook)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,2 +0,0 @@
|
||||||
module Api::V1::WebhooksHelper
|
|
||||||
end
|
|
7
app/jobs/webhook_job.rb
Normal file
7
app/jobs/webhook_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class WebhookJob < ApplicationJob
|
||||||
|
queue_as :webhooks
|
||||||
|
|
||||||
|
def perform(url, payload)
|
||||||
|
Webhooks::Trigger.execute(url, payload)
|
||||||
|
end
|
||||||
|
end
|
15
app/listeners/webhook_listener.rb
Normal file
15
app/listeners/webhook_listener.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
class WebhookListener < BaseListener
|
||||||
|
def message_created(event)
|
||||||
|
message = extract_message_and_account(event)[0]
|
||||||
|
inbox = message.inbox
|
||||||
|
|
||||||
|
return unless message.reportable? && inbox.webhook.present?
|
||||||
|
|
||||||
|
webhook = message.inbox.webhook
|
||||||
|
payload = message.push_event_data
|
||||||
|
|
||||||
|
webhook.urls.each do |url|
|
||||||
|
WebhookJob.perform_later(url, payload)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -22,6 +22,7 @@ class Account < ApplicationRecord
|
||||||
has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget'
|
has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget'
|
||||||
has_many :telegram_bots, dependent: :destroy
|
has_many :telegram_bots, dependent: :destroy
|
||||||
has_many :canned_responses, dependent: :destroy
|
has_many :canned_responses, dependent: :destroy
|
||||||
|
has_many :webhooks, dependent: :destroy
|
||||||
has_one :subscription, dependent: :destroy
|
has_one :subscription, dependent: :destroy
|
||||||
|
|
||||||
after_create :create_subscription
|
after_create :create_subscription
|
||||||
|
|
|
@ -32,6 +32,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
|
||||||
after_create :subscribe_webhook, if: :facebook?
|
after_create :subscribe_webhook, if: :facebook?
|
||||||
after_destroy :delete_round_robin_agents
|
after_destroy :delete_round_robin_agents
|
||||||
|
|
||||||
|
|
20
app/models/webhook.rb
Normal file
20
app/models/webhook.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: webhooks
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# urls :string
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :integer
|
||||||
|
# inbox_id :integer
|
||||||
|
#
|
||||||
|
|
||||||
|
class Webhook < ApplicationRecord
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :inbox
|
||||||
|
|
||||||
|
validates :account_id, presence: true
|
||||||
|
validates :inbox_id, presence: true
|
||||||
|
serialize :urls, Array
|
||||||
|
end
|
17
app/policies/webhook_policy.rb
Normal file
17
app/policies/webhook_policy.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
class WebhookPolicy < ApplicationPolicy
|
||||||
|
def index?
|
||||||
|
@user.administrator?
|
||||||
|
end
|
||||||
|
|
||||||
|
def update?
|
||||||
|
@user.administrator?
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
@user.administrator?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create?
|
||||||
|
@user.administrator?
|
||||||
|
end
|
||||||
|
end
|
10
app/views/api/v1/inbox/index.json.jbuilder
Normal file
10
app/views/api/v1/inbox/index.json.jbuilder
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
json.array! @agents do |agent|
|
||||||
|
json.account_id agent.account_id
|
||||||
|
json.availability_status agent.availability_status
|
||||||
|
json.confirmed agent.confirmed?
|
||||||
|
json.email agent.email
|
||||||
|
json.id agent.id
|
||||||
|
json.name agent.name
|
||||||
|
json.role agent.role
|
||||||
|
json.thumbnail agent.avatar_url
|
||||||
|
end
|
7
app/views/api/v1/inbox/webhooks/_webhook.json.jbuilder
Normal file
7
app/views/api/v1/inbox/webhooks/_webhook.json.jbuilder
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
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
|
5
app/views/api/v1/inbox/webhooks/create.json.jbuilder
Normal file
5
app/views/api/v1/inbox/webhooks/create.json.jbuilder
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
json.payload do
|
||||||
|
json.webhook do
|
||||||
|
json.partial! 'webhook', webhook: @webhook
|
||||||
|
end
|
||||||
|
end
|
5
app/views/api/v1/inbox/webhooks/index.json.jbuilder
Normal file
5
app/views/api/v1/inbox/webhooks/index.json.jbuilder
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
json.payload do
|
||||||
|
json.webhooks do
|
||||||
|
json.array! @webhooks, partial: 'webhooks/webhook', as: :webhook
|
||||||
|
end
|
||||||
|
end
|
5
app/views/api/v1/inbox/webhooks/update.json.jbuilder
Normal file
5
app/views/api/v1/inbox/webhooks/update.json.jbuilder
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
json.payload do
|
||||||
|
json.webhook do
|
||||||
|
json.partial! 'webhook', webhook: @webhook
|
||||||
|
end
|
||||||
|
end
|
|
@ -40,6 +40,10 @@ Rails.application.routes.draw do
|
||||||
resource :contact_merge, only: [:create]
|
resource :contact_merge, only: [:create]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
namespace :inbox do
|
||||||
|
resources :webhooks, except: [:show]
|
||||||
|
end
|
||||||
|
|
||||||
resource :profile, only: [:show, :update]
|
resource :profile, only: [:show, :update]
|
||||||
resources :accounts, only: [:create]
|
resources :accounts, only: [:create]
|
||||||
resources :inboxes, only: [:index, :destroy]
|
resources :inboxes, only: [:index, :destroy]
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
- [default, 2]
|
- [default, 2]
|
||||||
- [low, 1]
|
- [low, 1]
|
||||||
- [mailers, 2]
|
- [mailers, 2]
|
||||||
|
- [webhooks, 1]
|
||||||
|
|
||||||
# you can override concurrency based on environment
|
# you can override concurrency based on environment
|
||||||
production:
|
production:
|
||||||
|
|
11
db/migrate/20200213054733_create_webhooks.rb
Normal file
11
db/migrate/20200213054733_create_webhooks.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
class CreateWebhooks < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
create_table :webhooks do |t|
|
||||||
|
t.integer :account_id
|
||||||
|
t.integer :inbox_id
|
||||||
|
t.string :urls
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
433
db/schema.rb
433
db/schema.rb
|
@ -10,256 +10,265 @@
|
||||||
#
|
#
|
||||||
# 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_06_182948) do
|
ActiveRecord::Schema.define(version: 20_200_213_054_733) 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'
|
||||||
|
|
||||||
create_table "accounts", id: :serial, force: :cascade do |t|
|
create_table 'accounts', id: :serial, force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string 'name', null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "active_storage_attachments", force: :cascade do |t|
|
create_table 'active_storage_attachments', force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string 'name', null: false
|
||||||
t.string "record_type", null: false
|
t.string 'record_type', null: false
|
||||||
t.bigint "record_id", null: false
|
t.bigint 'record_id', null: false
|
||||||
t.bigint "blob_id", null: false
|
t.bigint 'blob_id', null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
|
t.index ['blob_id'], name: 'index_active_storage_attachments_on_blob_id'
|
||||||
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
|
t.index %w[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "active_storage_blobs", force: :cascade do |t|
|
create_table 'active_storage_blobs', force: :cascade do |t|
|
||||||
t.string "key", null: false
|
t.string 'key', null: false
|
||||||
t.string "filename", null: false
|
t.string 'filename', null: false
|
||||||
t.string "content_type"
|
t.string 'content_type'
|
||||||
t.text "metadata"
|
t.text 'metadata'
|
||||||
t.bigint "byte_size", null: false
|
t.bigint 'byte_size', null: false
|
||||||
t.string "checksum", null: false
|
t.string 'checksum', null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
|
t.index ['key'], name: 'index_active_storage_blobs_on_key', unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "attachments", id: :serial, force: :cascade do |t|
|
create_table 'attachments', id: :serial, force: :cascade do |t|
|
||||||
t.integer "file_type", default: 0
|
t.integer 'file_type', default: 0
|
||||||
t.string "external_url"
|
t.string 'external_url'
|
||||||
t.float "coordinates_lat", default: 0.0
|
t.float 'coordinates_lat', default: 0.0
|
||||||
t.float "coordinates_long", default: 0.0
|
t.float 'coordinates_long', default: 0.0
|
||||||
t.integer "message_id", null: false
|
t.integer 'message_id', null: false
|
||||||
t.integer "account_id", null: false
|
t.integer 'account_id', null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
t.string "fallback_title"
|
t.string 'fallback_title'
|
||||||
t.string "extension"
|
t.string 'extension'
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "canned_responses", id: :serial, force: :cascade do |t|
|
create_table 'canned_responses', id: :serial, force: :cascade do |t|
|
||||||
t.integer "account_id", null: false
|
t.integer 'account_id', null: false
|
||||||
t.string "short_code"
|
t.string 'short_code'
|
||||||
t.text "content"
|
t.text 'content'
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "channel_facebook_pages", id: :serial, force: :cascade do |t|
|
create_table 'channel_facebook_pages', id: :serial, force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string 'name', null: false
|
||||||
t.string "page_id", null: false
|
t.string 'page_id', null: false
|
||||||
t.string "user_access_token", null: false
|
t.string 'user_access_token', null: false
|
||||||
t.string "page_access_token", null: false
|
t.string 'page_access_token', null: false
|
||||||
t.integer "account_id", null: false
|
t.integer 'account_id', null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
t.index ["page_id", "account_id"], name: "index_channel_facebook_pages_on_page_id_and_account_id", unique: true
|
t.index %w[page_id account_id], name: 'index_channel_facebook_pages_on_page_id_and_account_id', unique: true
|
||||||
t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id"
|
t.index ['page_id'], name: 'index_channel_facebook_pages_on_page_id'
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "channel_twitter_profiles", force: :cascade do |t|
|
create_table 'channel_twitter_profiles', force: :cascade do |t|
|
||||||
t.string "name"
|
t.string 'name'
|
||||||
t.string "profile_id", null: false
|
t.string 'profile_id', null: false
|
||||||
t.string "twitter_access_token", null: false
|
t.string 'twitter_access_token', null: false
|
||||||
t.string "twitter_access_token_secret", null: false
|
t.string 'twitter_access_token_secret', null: false
|
||||||
t.integer "account_id", null: false
|
t.integer 'account_id', null: false
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "channel_web_widgets", id: :serial, force: :cascade do |t|
|
create_table 'channel_web_widgets', id: :serial, force: :cascade do |t|
|
||||||
t.string "website_name"
|
t.string 'website_name'
|
||||||
t.string "website_url"
|
t.string 'website_url'
|
||||||
t.integer "account_id"
|
t.integer 'account_id'
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
t.string "website_token"
|
t.string 'website_token'
|
||||||
t.string "widget_color", default: "#1f93ff"
|
t.string 'widget_color', default: '#1f93ff'
|
||||||
t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true
|
t.index ['website_token'], name: 'index_channel_web_widgets_on_website_token', unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "contact_inboxes", force: :cascade do |t|
|
create_table 'contact_inboxes', force: :cascade do |t|
|
||||||
t.bigint "contact_id"
|
t.bigint 'contact_id'
|
||||||
t.bigint "inbox_id"
|
t.bigint 'inbox_id'
|
||||||
t.string "source_id", null: false
|
t.string 'source_id', null: false
|
||||||
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.index ["contact_id"], name: "index_contact_inboxes_on_contact_id"
|
t.index ['contact_id'], name: 'index_contact_inboxes_on_contact_id'
|
||||||
t.index ["inbox_id", "source_id"], name: "index_contact_inboxes_on_inbox_id_and_source_id", unique: true
|
t.index %w[inbox_id source_id], name: 'index_contact_inboxes_on_inbox_id_and_source_id', unique: true
|
||||||
t.index ["inbox_id"], name: "index_contact_inboxes_on_inbox_id"
|
t.index ['inbox_id'], name: 'index_contact_inboxes_on_inbox_id'
|
||||||
t.index ["source_id"], name: "index_contact_inboxes_on_source_id"
|
t.index ['source_id'], name: 'index_contact_inboxes_on_source_id'
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "contacts", id: :serial, force: :cascade do |t|
|
create_table 'contacts', id: :serial, force: :cascade do |t|
|
||||||
t.string "name"
|
t.string 'name'
|
||||||
t.string "email"
|
t.string 'email'
|
||||||
t.string "phone_number"
|
t.string 'phone_number'
|
||||||
t.integer "account_id", null: false
|
t.integer 'account_id', null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
t.string "pubsub_token"
|
t.string 'pubsub_token'
|
||||||
t.jsonb "additional_attributes"
|
t.jsonb 'additional_attributes'
|
||||||
t.index ["account_id"], name: "index_contacts_on_account_id"
|
t.index ['account_id'], name: 'index_contacts_on_account_id'
|
||||||
t.index ["pubsub_token"], name: "index_contacts_on_pubsub_token", unique: true
|
t.index ['pubsub_token'], name: 'index_contacts_on_pubsub_token', unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "conversations", id: :serial, force: :cascade do |t|
|
create_table 'conversations', id: :serial, force: :cascade do |t|
|
||||||
t.integer "account_id", null: false
|
t.integer 'account_id', null: false
|
||||||
t.integer "inbox_id", null: false
|
t.integer 'inbox_id', null: false
|
||||||
t.integer "status", default: 0, null: false
|
t.integer 'status', default: 0, null: false
|
||||||
t.integer "assignee_id"
|
t.integer 'assignee_id'
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
t.bigint "contact_id"
|
t.bigint 'contact_id'
|
||||||
t.integer "display_id", null: false
|
t.integer 'display_id', null: false
|
||||||
t.datetime "user_last_seen_at"
|
t.datetime 'user_last_seen_at'
|
||||||
t.datetime "agent_last_seen_at"
|
t.datetime 'agent_last_seen_at'
|
||||||
t.boolean "locked", default: false
|
t.boolean 'locked', default: false
|
||||||
t.jsonb "additional_attributes"
|
t.jsonb 'additional_attributes'
|
||||||
t.bigint "contact_inbox_id"
|
t.bigint 'contact_inbox_id'
|
||||||
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
|
t.index %w[account_id display_id], name: 'index_conversations_on_account_id_and_display_id', unique: true
|
||||||
t.index ["account_id"], name: "index_conversations_on_account_id"
|
t.index ['account_id'], name: 'index_conversations_on_account_id'
|
||||||
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
|
t.index ['contact_inbox_id'], name: 'index_conversations_on_contact_inbox_id'
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "inbox_members", id: :serial, force: :cascade do |t|
|
create_table 'inbox_members', id: :serial, force: :cascade do |t|
|
||||||
t.integer "user_id", null: false
|
t.integer 'user_id', null: false
|
||||||
t.integer "inbox_id", null: false
|
t.integer 'inbox_id', null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
t.index ["inbox_id"], name: "index_inbox_members_on_inbox_id"
|
t.index ['inbox_id'], name: 'index_inbox_members_on_inbox_id'
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "inboxes", id: :serial, force: :cascade do |t|
|
create_table 'inboxes', id: :serial, force: :cascade do |t|
|
||||||
t.integer "channel_id", null: false
|
t.integer 'channel_id', null: false
|
||||||
t.integer "account_id", null: false
|
t.integer 'account_id', null: false
|
||||||
t.string "name", null: false
|
t.string 'name', null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
t.string "channel_type"
|
t.string 'channel_type'
|
||||||
t.index ["account_id"], name: "index_inboxes_on_account_id"
|
t.index ['account_id'], name: 'index_inboxes_on_account_id'
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "messages", id: :serial, force: :cascade do |t|
|
create_table 'messages', id: :serial, force: :cascade do |t|
|
||||||
t.text "content"
|
t.text 'content'
|
||||||
t.integer "account_id", null: false
|
t.integer 'account_id', null: false
|
||||||
t.integer "inbox_id", null: false
|
t.integer 'inbox_id', null: false
|
||||||
t.integer "conversation_id", null: false
|
t.integer 'conversation_id', null: false
|
||||||
t.integer "message_type", null: false
|
t.integer 'message_type', null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
t.boolean "private", default: false
|
t.boolean 'private', default: false
|
||||||
t.integer "user_id"
|
t.integer 'user_id'
|
||||||
t.integer "status", default: 0
|
t.integer 'status', default: 0
|
||||||
t.string "source_id"
|
t.string 'source_id'
|
||||||
t.integer "content_type", default: 0
|
t.integer 'content_type', default: 0
|
||||||
t.json "content_attributes", default: {}
|
t.json 'content_attributes', default: {}
|
||||||
t.bigint "contact_id"
|
t.bigint 'contact_id'
|
||||||
t.index ["contact_id"], name: "index_messages_on_contact_id"
|
t.index ['contact_id'], name: 'index_messages_on_contact_id'
|
||||||
t.index ["conversation_id"], name: "index_messages_on_conversation_id"
|
t.index ['conversation_id'], name: 'index_messages_on_conversation_id'
|
||||||
t.index ["source_id"], name: "index_messages_on_source_id"
|
t.index ['source_id'], name: 'index_messages_on_source_id'
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "subscriptions", id: :serial, force: :cascade do |t|
|
create_table 'subscriptions', id: :serial, force: :cascade do |t|
|
||||||
t.string "pricing_version"
|
t.string 'pricing_version'
|
||||||
t.integer "account_id"
|
t.integer 'account_id'
|
||||||
t.datetime "expiry"
|
t.datetime 'expiry'
|
||||||
t.string "billing_plan", default: "trial"
|
t.string 'billing_plan', default: 'trial'
|
||||||
t.string "stripe_customer_id"
|
t.string 'stripe_customer_id'
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
t.integer "state", default: 0
|
t.integer 'state', default: 0
|
||||||
t.boolean "payment_source_added", default: false
|
t.boolean 'payment_source_added', default: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "taggings", id: :serial, force: :cascade do |t|
|
create_table 'taggings', id: :serial, force: :cascade do |t|
|
||||||
t.integer "tag_id"
|
t.integer 'tag_id'
|
||||||
t.string "taggable_type"
|
t.string 'taggable_type'
|
||||||
t.integer "taggable_id"
|
t.integer 'taggable_id'
|
||||||
t.string "tagger_type"
|
t.string 'tagger_type'
|
||||||
t.integer "tagger_id"
|
t.integer 'tagger_id'
|
||||||
t.string "context", limit: 128
|
t.string 'context', limit: 128
|
||||||
t.datetime "created_at"
|
t.datetime 'created_at'
|
||||||
t.index ["context"], name: "index_taggings_on_context"
|
t.index ['context'], name: 'index_taggings_on_context'
|
||||||
t.index ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true
|
t.index %w[tag_id taggable_id taggable_type context tagger_id tagger_type], name: 'taggings_idx', unique: true
|
||||||
t.index ["tag_id"], name: "index_taggings_on_tag_id"
|
t.index ['tag_id'], name: 'index_taggings_on_tag_id'
|
||||||
t.index ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context"
|
t.index %w[taggable_id taggable_type context], name: 'index_taggings_on_taggable_id_and_taggable_type_and_context'
|
||||||
t.index ["taggable_id", "taggable_type", "tagger_id", "context"], name: "taggings_idy"
|
t.index %w[taggable_id taggable_type tagger_id context], name: 'taggings_idy'
|
||||||
t.index ["taggable_id"], name: "index_taggings_on_taggable_id"
|
t.index ['taggable_id'], name: 'index_taggings_on_taggable_id'
|
||||||
t.index ["taggable_type"], name: "index_taggings_on_taggable_type"
|
t.index %w[taggable_type taggable_id], name: 'index_taggings_on_taggable_type_and_taggable_id'
|
||||||
t.index ["tagger_id", "tagger_type"], name: "index_taggings_on_tagger_id_and_tagger_type"
|
t.index ['taggable_type'], name: 'index_taggings_on_taggable_type'
|
||||||
t.index ["tagger_id"], name: "index_taggings_on_tagger_id"
|
t.index %w[tagger_id tagger_type], name: 'index_taggings_on_tagger_id_and_tagger_type'
|
||||||
|
t.index ['tagger_id'], name: 'index_taggings_on_tagger_id'
|
||||||
|
t.index %w[tagger_type tagger_id], name: 'index_taggings_on_tagger_type_and_tagger_id'
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "tags", id: :serial, force: :cascade do |t|
|
create_table 'tags', id: :serial, force: :cascade do |t|
|
||||||
t.string "name"
|
t.string 'name'
|
||||||
t.integer "taggings_count", default: 0
|
t.integer 'taggings_count', default: 0
|
||||||
t.index ["name"], name: "index_tags_on_name", unique: true
|
t.index ['name'], name: 'index_tags_on_name', unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "telegram_bots", id: :serial, force: :cascade do |t|
|
create_table 'telegram_bots', id: :serial, force: :cascade do |t|
|
||||||
t.string "name"
|
t.string 'name'
|
||||||
t.string "auth_key"
|
t.string 'auth_key'
|
||||||
t.integer "account_id"
|
t.integer 'account_id'
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "users", id: :serial, force: :cascade do |t|
|
create_table 'users', id: :serial, force: :cascade do |t|
|
||||||
t.string "provider", default: "email", null: false
|
t.string 'provider', default: 'email', null: false
|
||||||
t.string "uid", default: "", null: false
|
t.string 'uid', default: '', null: false
|
||||||
t.string "encrypted_password", default: "", null: false
|
t.string 'encrypted_password', default: '', null: false
|
||||||
t.string "reset_password_token"
|
t.string 'reset_password_token'
|
||||||
t.datetime "reset_password_sent_at"
|
t.datetime 'reset_password_sent_at'
|
||||||
t.datetime "remember_created_at"
|
t.datetime 'remember_created_at'
|
||||||
t.integer "sign_in_count", default: 0, null: false
|
t.integer 'sign_in_count', default: 0, null: false
|
||||||
t.datetime "current_sign_in_at"
|
t.datetime 'current_sign_in_at'
|
||||||
t.datetime "last_sign_in_at"
|
t.datetime 'last_sign_in_at'
|
||||||
t.string "current_sign_in_ip"
|
t.string 'current_sign_in_ip'
|
||||||
t.string "last_sign_in_ip"
|
t.string 'last_sign_in_ip'
|
||||||
t.string "confirmation_token"
|
t.string 'confirmation_token'
|
||||||
t.datetime "confirmed_at"
|
t.datetime 'confirmed_at'
|
||||||
t.datetime "confirmation_sent_at"
|
t.datetime 'confirmation_sent_at'
|
||||||
t.string "unconfirmed_email"
|
t.string 'unconfirmed_email'
|
||||||
t.string "name", null: false
|
t.string 'name', null: false
|
||||||
t.string "nickname"
|
t.string 'nickname'
|
||||||
t.string "email"
|
t.string 'email'
|
||||||
t.json "tokens"
|
t.json 'tokens'
|
||||||
t.integer "account_id", null: false
|
t.integer 'account_id', null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime 'created_at', null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime 'updated_at', null: false
|
||||||
t.string "pubsub_token"
|
t.string 'pubsub_token'
|
||||||
t.integer "role", default: 0
|
t.integer 'role', default: 0
|
||||||
t.bigint "inviter_id"
|
t.bigint 'inviter_id'
|
||||||
t.index ["email"], name: "index_users_on_email"
|
t.index ['email'], name: 'index_users_on_email'
|
||||||
t.index ["inviter_id"], name: "index_users_on_inviter_id"
|
t.index ['inviter_id'], name: 'index_users_on_inviter_id'
|
||||||
t.index ["pubsub_token"], name: "index_users_on_pubsub_token", unique: true
|
t.index ['pubsub_token'], name: 'index_users_on_pubsub_token', unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true
|
||||||
t.index ["uid", "provider"], name: "index_users_on_uid_and_provider", unique: true
|
t.index %w[uid provider], name: 'index_users_on_uid_and_provider', unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
create_table 'webhooks', force: :cascade do |t|
|
||||||
add_foreign_key "contact_inboxes", "contacts"
|
t.integer 'account_id'
|
||||||
add_foreign_key "contact_inboxes", "inboxes"
|
t.integer 'inbox_id'
|
||||||
add_foreign_key "conversations", "contact_inboxes"
|
t.string 'urls'
|
||||||
add_foreign_key "messages", "contacts"
|
t.datetime 'created_at', precision: 6, null: false
|
||||||
add_foreign_key "users", "users", column: "inviter_id", on_delete: :nullify
|
t.datetime 'updated_at', precision: 6, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_foreign_key 'active_storage_attachments', 'active_storage_blobs', column: 'blob_id'
|
||||||
|
add_foreign_key 'contact_inboxes', 'contacts'
|
||||||
|
add_foreign_key 'contact_inboxes', 'inboxes'
|
||||||
|
add_foreign_key 'conversations', 'contact_inboxes'
|
||||||
|
add_foreign_key 'messages', 'contacts'
|
||||||
|
add_foreign_key 'users', 'users', column: 'inviter_id', on_delete: :nullify
|
||||||
end
|
end
|
||||||
|
|
7
lib/webhooks/trigger.rb
Normal file
7
lib/webhooks/trigger.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class Webhooks::Trigger
|
||||||
|
def self.execute(url, payload)
|
||||||
|
RestClient.post(url, payload)
|
||||||
|
rescue StandardError => e
|
||||||
|
Raven.capture_exception(e)
|
||||||
|
end
|
||||||
|
end
|
104
spec/controllers/api/v1/inbox/webhook_controller_spec.rb
Normal file
104
spec/controllers/api/v1/inbox/webhook_controller_spec.rb
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Webhooks API', type: :request do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:inbox) { create(:inbox, account: account) }
|
||||||
|
let(:webhook) { create(:webhook, account: account, inbox: inbox, urls: ['https://hello.com']) }
|
||||||
|
let(:administrator) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:agent) { create(:user, account: account, role: :agent) }
|
||||||
|
|
||||||
|
describe 'GET /api/v1/inbox/webhooks' do
|
||||||
|
context 'when it is an authenticated agent' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
get '/api/v1/inbox/webhooks',
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an authenticated admin user' do
|
||||||
|
it 'gets all webhook' do
|
||||||
|
get '/api/v1/inbox/webhooks',
|
||||||
|
headers: administrator.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(JSON.parse(response.body)['payload']['webhooks'].count).to eql account.webhooks.count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/inbox/webhooks' do
|
||||||
|
context 'when it is an authenticated agent' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
post '/api/v1/inbox/webhooks',
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an authenticated admin user' do
|
||||||
|
it 'creates webhook' do
|
||||||
|
post '/api/v1/inbox/webhooks',
|
||||||
|
params: { account_id: account.id, inbox_id: inbox.id, urls: ['https://hello.com'] },
|
||||||
|
headers: administrator.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
|
||||||
|
expect(JSON.parse(response.body)['payload']['webhook']['urls']).to eql ['https://hello.com']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT /api/v1/inbox/webhooks/:id' do
|
||||||
|
context 'when it is an authenticated agent' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
put "/api/v1/inbox/webhooks/#{webhook.id}",
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an authenticated admin user' do
|
||||||
|
it 'updates webhook' do
|
||||||
|
put "/api/v1/inbox/webhooks/#{webhook.id}",
|
||||||
|
params: { urls: ['https://hello.com', 'https://world.com'] },
|
||||||
|
headers: administrator.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(JSON.parse(response.body)['payload']['webhook']['urls']).to eql ['https://hello.com', 'https://world.com']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /api/v1/inbox/webhooks/:id' do
|
||||||
|
context 'when it is an authenticated agent' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
delete "/api/v1/inbox/webhooks/#{webhook.id}",
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when it is an authenticated admin user' do
|
||||||
|
it 'deletes webhook' do
|
||||||
|
delete "/api/v1/inbox/webhooks/#{webhook.id}",
|
||||||
|
headers: administrator.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(account.webhooks.count).to be 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
7
spec/factories/webhooks.rb
Normal file
7
spec/factories/webhooks.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :webhook do
|
||||||
|
account_id { 1 }
|
||||||
|
inbox_id { 1 }
|
||||||
|
urls { ['MyString'] }
|
||||||
|
end
|
||||||
|
end
|
14
spec/jobs/webhook_job_spec.rb
Normal file
14
spec/jobs/webhook_job_spec.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe WebhookJob, type: :job do
|
||||||
|
subject(:job) { described_class.perform_later(url, payload) }
|
||||||
|
|
||||||
|
let(:url) { 'https://test.com' }
|
||||||
|
let(:payload) { { name: 'test' } }
|
||||||
|
|
||||||
|
it 'queues the job' do
|
||||||
|
expect { job }.to have_enqueued_job(described_class)
|
||||||
|
.with(url, payload)
|
||||||
|
.on_queue('webhooks')
|
||||||
|
end
|
||||||
|
end
|
15
spec/lib/webhooks/trigger_spec.rb
Normal file
15
spec/lib/webhooks/trigger_spec.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Webhooks::Trigger do
|
||||||
|
subject(:trigger) { described_class }
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
it 'triggers webhook' do
|
||||||
|
params = { hello: 'hello' }
|
||||||
|
url = 'htpps://test.com'
|
||||||
|
|
||||||
|
expect(RestClient).to receive(:post).with(url, params).once
|
||||||
|
trigger.execute(url, params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
33
spec/listeners/webhook_listener_spec.rb
Normal file
33
spec/listeners/webhook_listener_spec.rb
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
describe WebhookListener do
|
||||||
|
let(:listener) { described_class.instance }
|
||||||
|
let!(:account) { create(:account) }
|
||||||
|
let(:report_identity) { Reports::UpdateAccountIdentity.new(account, Time.zone.now) }
|
||||||
|
let!(:user) { create(:user, account: account) }
|
||||||
|
let!(:inbox) { create(:inbox, account: account) }
|
||||||
|
let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user) }
|
||||||
|
let!(:message) do
|
||||||
|
create(:message, message_type: 'outgoing',
|
||||||
|
account: account, inbox: inbox, conversation: conversation)
|
||||||
|
end
|
||||||
|
let!(:event) { Events::Base.new(event_name, Time.zone.now, message: message) }
|
||||||
|
|
||||||
|
describe '#message_created' do
|
||||||
|
let(:event_name) { :'conversation.created' }
|
||||||
|
|
||||||
|
context 'when webhook is not configured' do
|
||||||
|
it 'does not trigger webhook' do
|
||||||
|
expect(RestClient).to receive(:post).exactly(0).times
|
||||||
|
listener.message_created(event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when webhook is configured' do
|
||||||
|
it 'triggers webhook' do
|
||||||
|
create(:webhook, inbox: inbox, account: account)
|
||||||
|
expect(WebhookJob).to receive(:perform_later).once
|
||||||
|
listener.message_created(event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,4 +14,5 @@ RSpec.describe Account do
|
||||||
it { is_expected.to have_many(:facebook_pages).class_name('::Channel::FacebookPage').dependent(:destroy) }
|
it { is_expected.to have_many(:facebook_pages).class_name('::Channel::FacebookPage').dependent(:destroy) }
|
||||||
it { is_expected.to have_many(:web_widgets).class_name('::Channel::WebWidget').dependent(:destroy) }
|
it { is_expected.to have_many(:web_widgets).class_name('::Channel::WebWidget').dependent(:destroy) }
|
||||||
it { is_expected.to have_one(:subscription).dependent(:destroy) }
|
it { is_expected.to have_one(:subscription).dependent(:destroy) }
|
||||||
|
it { is_expected.to have_many(:webhooks).dependent(:destroy) }
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,6 +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) }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#add_member' do
|
describe '#add_member' do
|
||||||
|
|
13
spec/models/webhook_spec.rb
Normal file
13
spec/models/webhook_spec.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Webhook, type: :model do
|
||||||
|
describe 'validations' do
|
||||||
|
it { is_expected.to validate_presence_of(:account_id) }
|
||||||
|
it { is_expected.to validate_presence_of(:inbox_id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'associations' do
|
||||||
|
it { is_expected.to belong_to(:account) }
|
||||||
|
it { is_expected.to belong_to(:inbox) }
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue