Feature: Slack integration (#783)
- Integrations architecture - Slack integration
This commit is contained in:
@@ -85,6 +85,10 @@ TWITTER_CONSUMER_KEY=
|
||||
TWITTER_CONSUMER_SECRET=
|
||||
TWITTER_ENVIRONMENT=
|
||||
|
||||
#slack
|
||||
SLACK_CLIENT_ID=
|
||||
SLACK_CLIENT_SECRET
|
||||
|
||||
### Change this env variable only if you are using a custom build mobile app
|
||||
## Mobile app env variables
|
||||
IOS_APP_ID=6C953F3RX2.com.chatwoot.app
|
||||
|
||||
@@ -65,6 +65,8 @@ gem 'twilio-ruby', '~> 5.32.0'
|
||||
gem 'twitty'
|
||||
# facebook client
|
||||
gem 'koala'
|
||||
# slack client
|
||||
gem 'slack-ruby-client'
|
||||
# Random name generator
|
||||
gem 'haikunator'
|
||||
|
||||
@@ -115,4 +117,5 @@ group :development, :test do
|
||||
gem 'simplecov', '0.17.1', require: false
|
||||
gem 'spring'
|
||||
gem 'spring-watcher-listen'
|
||||
gem 'webmock'
|
||||
end
|
||||
|
||||
@@ -143,6 +143,8 @@ GEM
|
||||
descendants_tracker (~> 0.0.1)
|
||||
concurrent-ruby (1.1.6)
|
||||
connection_pool (2.2.2)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.6)
|
||||
datetime_picker_rails (0.0.7)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
@@ -191,6 +193,7 @@ GEM
|
||||
ffi (1.12.2)
|
||||
flag_shih_tzu (0.3.23)
|
||||
foreman (0.87.1)
|
||||
gli (2.19.0)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
google-api-client (0.39.4)
|
||||
@@ -225,6 +228,7 @@ GEM
|
||||
activesupport (>= 5)
|
||||
haikunator (1.1.0)
|
||||
hana (1.3.6)
|
||||
hashdiff (1.0.1)
|
||||
hashie (4.1.0)
|
||||
hkdf (0.3.0)
|
||||
http-accept (1.7.0)
|
||||
@@ -411,6 +415,7 @@ GEM
|
||||
rubocop-rspec (1.39.0)
|
||||
rubocop (>= 0.68.1)
|
||||
ruby-progressbar (1.10.1)
|
||||
safe_yaml (1.0.5)
|
||||
sass (3.7.4)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
@@ -452,6 +457,13 @@ GEM
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.2)
|
||||
slack-ruby-client (0.14.6)
|
||||
activesupport
|
||||
faraday (>= 0.9)
|
||||
faraday_middleware
|
||||
gli
|
||||
hashie
|
||||
websocket-driver
|
||||
spring (2.1.0)
|
||||
spring-watcher-listen (2.0.1)
|
||||
listen (>= 2.7, < 4.0)
|
||||
@@ -507,6 +519,10 @@ GEM
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.8.3)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (5.1.1)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
@@ -583,6 +599,7 @@ DEPENDENCIES
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
simplecov (= 0.17.1)
|
||||
slack-ruby-client
|
||||
spring
|
||||
spring-watcher-listen
|
||||
telegram-bot-ruby
|
||||
@@ -594,6 +611,7 @@ DEPENDENCIES
|
||||
uglifier
|
||||
valid_email2
|
||||
web-console
|
||||
webmock
|
||||
webpacker
|
||||
webpush
|
||||
wisper (= 2.0.0)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_apps, only: [:index]
|
||||
before_action :fetch_app, only: [:show]
|
||||
|
||||
def index; end
|
||||
|
||||
def show; end
|
||||
|
||||
private
|
||||
|
||||
def fetch_apps
|
||||
@apps = Integrations::App.all
|
||||
end
|
||||
|
||||
def fetch_app
|
||||
@app = Integrations::App.find(id: params[:id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
class Api::V1::Accounts::Integrations::SlackController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_hook, only: [:update, :destroy]
|
||||
|
||||
def create
|
||||
builder = Integrations::Slack::HookBuilder.new(
|
||||
account: current_account,
|
||||
code: params[:code],
|
||||
inbox_id: params[:inbox_id]
|
||||
)
|
||||
|
||||
@hook = builder.perform
|
||||
|
||||
render json: @hook
|
||||
end
|
||||
|
||||
def update
|
||||
builder = Integrations::Slack::ChannelBuilder.new(
|
||||
hook: @hook, channel: params[:channel]
|
||||
)
|
||||
builder.perform
|
||||
|
||||
render json: @hook
|
||||
end
|
||||
|
||||
def destroy
|
||||
@hook.destroy
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_hook
|
||||
@hook = Integrations::Hook.find(params[:id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class Api::V1::Integrations::WebhooksController < ApplicationController
|
||||
def create
|
||||
builder = Integrations::Slack::IncomingMessageBuilder.new(params)
|
||||
response = builder.perform
|
||||
render json: response
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,7 @@ class AsyncDispatcher < BaseDispatcher
|
||||
end
|
||||
|
||||
def listeners
|
||||
listeners = [EventListener.instance, WebhookListener.instance]
|
||||
listeners = [EventListener.instance, WebhookListener.instance, HookListener.instance]
|
||||
listeners
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
class HookJob < ApplicationJob
|
||||
queue_as :integrations
|
||||
|
||||
def perform(hook, message)
|
||||
return unless hook.slack?
|
||||
|
||||
Integrations::Slack::OutgoingMessageBuilder.perform(hook, message)
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
class HookListener < BaseListener
|
||||
def message_created(event)
|
||||
message = extract_message_and_account(event)[0]
|
||||
return unless message.reportable?
|
||||
|
||||
message.account.hooks.each do |hook|
|
||||
HookJob.perform_later(hook, message)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -47,6 +47,7 @@ class Account < ApplicationRecord
|
||||
has_many :webhooks, dependent: :destroy
|
||||
has_many :labels, dependent: :destroy
|
||||
has_many :notification_settings, dependent: :destroy
|
||||
has_many :hooks, dependent: :destroy, class_name: 'Integrations::Hook'
|
||||
has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING)
|
||||
|
||||
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# id :integer not null, primary key
|
||||
# additional_attributes :jsonb
|
||||
# agent_last_seen_at :datetime
|
||||
# identifier :string
|
||||
# locked :boolean default(FALSE)
|
||||
# status :integer default("open"), not null
|
||||
# user_last_seen_at :datetime
|
||||
|
||||
@@ -42,6 +42,7 @@ class Inbox < ApplicationRecord
|
||||
has_one :agent_bot_inbox, dependent: :destroy
|
||||
has_one :agent_bot, through: :agent_bot_inbox
|
||||
has_many :webhooks, dependent: :destroy
|
||||
has_many :hooks, dependent: :destroy, class_name: 'Integrations::Hook'
|
||||
|
||||
after_destroy :delete_round_robin_agents
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
module Integrations
|
||||
def self.table_name_prefix
|
||||
'integrations_'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
class Integrations::App
|
||||
attr_accessor :params
|
||||
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def id
|
||||
params[:id]
|
||||
end
|
||||
|
||||
def name
|
||||
params[:name]
|
||||
end
|
||||
|
||||
def description
|
||||
params[:description]
|
||||
end
|
||||
|
||||
def logo
|
||||
params[:logo]
|
||||
end
|
||||
|
||||
def fields
|
||||
params[:fields]
|
||||
end
|
||||
|
||||
def button
|
||||
params[:button]
|
||||
end
|
||||
|
||||
def enabled?(account)
|
||||
account.hooks.where(app_id: id).exists?
|
||||
end
|
||||
|
||||
class << self
|
||||
def apps
|
||||
Hashie::Mash.new(APPS_CONFIG)
|
||||
end
|
||||
|
||||
def all
|
||||
apps.values.each_with_object([]) do |app, result|
|
||||
result << new(app)
|
||||
end
|
||||
end
|
||||
|
||||
def find(params)
|
||||
all.detect { |app| app.id == params[:id] }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: integrations_hooks
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# access_token :string
|
||||
# hook_type :integer default("account")
|
||||
# settings :text
|
||||
# status :integer default("disabled")
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer
|
||||
# app_id :string
|
||||
# inbox_id :integer
|
||||
# reference_id :string
|
||||
#
|
||||
class Integrations::Hook < ApplicationRecord
|
||||
validates :account_id, presence: true
|
||||
validates :app_id, presence: true
|
||||
|
||||
enum status: { disabled: 0, enabled: 1 }
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :inbox, optional: true
|
||||
has_secure_token :access_token
|
||||
|
||||
enum hook_type: { account: 0, inbox: 1 }
|
||||
|
||||
def app
|
||||
@app ||= Integrations::App.find(id: app_id)
|
||||
end
|
||||
|
||||
def slack?
|
||||
app_id == 'cw_slack'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
json.array! @apps do |app|
|
||||
json.id app.id
|
||||
json.name app.name
|
||||
json.logo app.logo
|
||||
json.enabled app.enabled?(@current_account)
|
||||
json.button app.button
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
json.id @app.id
|
||||
json.name @app.name
|
||||
json.logo @app.logo
|
||||
json.description @app.description
|
||||
json.fields @app.fields
|
||||
json.enabled @app.enabled?(@current_account)
|
||||
json.button @app.button
|
||||
@@ -0,0 +1 @@
|
||||
APPS_CONFIG = YAML.load_file(File.join(Rails.root, 'config/integration', 'apps.yml'))
|
||||
@@ -0,0 +1,6 @@
|
||||
slack:
|
||||
id: cw_slack
|
||||
name: Slack
|
||||
logo: https://a.slack-edge.com/80588/marketing/img/media-kit/img-logos@2x.png
|
||||
description: "'Be less busy' - Slack is the chat tool that brings all your communication together in one place. By integrating Slack with SupportBee, you can get notified in your Slack channels for important events in your support desk"
|
||||
button: <a href="https://slack.com/oauth/v2/authorize?scope=incoming-webhook,commands,chat:write&client_id=706921004289.1094198503990"><img alt=""Add to Slack"" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>
|
||||
+10
-2
@@ -87,18 +87,26 @@ Rails.application.routes.draw do
|
||||
end
|
||||
resource :notification_settings, only: [:show, :update]
|
||||
|
||||
resources :webhooks, except: [:show]
|
||||
resources :webhooks, except: [:show]
|
||||
namespace :integrations do
|
||||
resources :apps, only: [:index, :show]
|
||||
resources :slack, only: [:create, :update, :destroy]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# end of account scoped api routes
|
||||
# ----------------------------------
|
||||
|
||||
namespace :integrations do
|
||||
resources :webhooks, only: [:create]
|
||||
end
|
||||
|
||||
resource :profile, only: [:show, :update]
|
||||
resource :notification_subscriptions, only: [:create]
|
||||
|
||||
resources :agent_bots, only: [:index]
|
||||
|
||||
|
||||
namespace :widget do
|
||||
resources :events, only: [:create]
|
||||
resources :messages, only: [:index, :create, :update]
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
:queues:
|
||||
- [low, 1]
|
||||
- [webhooks, 1]
|
||||
- [integrations, 2]
|
||||
- [bots, 1]
|
||||
- [active_storage_analysis, 1]
|
||||
- [action_mailbox_incineration, 1]
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class CreateIntegrationsHooks < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
create_table :integrations_hooks do |t|
|
||||
t.integer :status, default: 0
|
||||
t.integer :inbox_id
|
||||
t.integer :account_id
|
||||
t.string :app_id
|
||||
t.text :settings
|
||||
t.integer :hook_type, default: 0
|
||||
t.string :reference_id
|
||||
t.string :access_token
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddReferenceIdToConversation < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
add_column :conversations, :reference_id, :string
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class RenameReferenceId < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
rename_column :conversations, :reference_id, :identifier
|
||||
end
|
||||
end
|
||||
+28
-2
@@ -10,10 +10,9 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2020_06_07_140737) do
|
||||
ActiveRecord::Schema.define(version: 2020_06_10_143132) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
|
||||
@@ -206,6 +205,7 @@ ActiveRecord::Schema.define(version: 2020_06_07_140737) do
|
||||
t.boolean "locked", default: false
|
||||
t.jsonb "additional_attributes"
|
||||
t.bigint "contact_inbox_id"
|
||||
t.string "identifier"
|
||||
t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
|
||||
t.index ["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"
|
||||
@@ -228,6 +228,17 @@ ActiveRecord::Schema.define(version: 2020_06_07_140737) do
|
||||
t.index ["user_id"], name: "index_events_on_user_id"
|
||||
end
|
||||
|
||||
create_table "hooks_inbox_apps", force: :cascade do |t|
|
||||
t.integer "inbox_id"
|
||||
t.integer "agent_id"
|
||||
t.integer "account_id"
|
||||
t.string "app_slug"
|
||||
t.string "status"
|
||||
t.text "settings"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
end
|
||||
|
||||
create_table "inbox_members", id: :serial, force: :cascade do |t|
|
||||
t.integer "user_id", null: false
|
||||
t.integer "inbox_id", null: false
|
||||
@@ -257,6 +268,19 @@ ActiveRecord::Schema.define(version: 2020_06_07_140737) do
|
||||
t.index ["name", "created_at"], name: "index_installation_configs_on_name_and_created_at", unique: true
|
||||
end
|
||||
|
||||
create_table "integrations_hooks", force: :cascade do |t|
|
||||
t.integer "status", default: 0
|
||||
t.integer "inbox_id"
|
||||
t.integer "account_id"
|
||||
t.string "app_id"
|
||||
t.text "settings"
|
||||
t.integer "hook_type", default: 0
|
||||
t.string "reference_id"
|
||||
t.string "access_token"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
end
|
||||
|
||||
create_table "labels", force: :cascade do |t|
|
||||
t.string "title"
|
||||
t.text "description"
|
||||
@@ -357,9 +381,11 @@ ActiveRecord::Schema.define(version: 2020_06_07_140737) do
|
||||
t.index ["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 ["taggable_id"], name: "index_taggings_on_taggable_id"
|
||||
t.index ["taggable_type", "taggable_id"], name: "index_taggings_on_taggable_type_and_taggable_id"
|
||||
t.index ["taggable_type"], name: "index_taggings_on_taggable_type"
|
||||
t.index ["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 ["tagger_type", "tagger_id"], name: "index_taggings_on_tagger_type_and_tagger_id"
|
||||
end
|
||||
|
||||
create_table "tags", id: :serial, force: :cascade do |t|
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
class Integrations::Slack::ChannelBuilder
|
||||
attr_reader :params, :channel
|
||||
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def perform
|
||||
create_channel
|
||||
update_reference_id
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def hook
|
||||
@hook ||= params[:hook]
|
||||
end
|
||||
|
||||
def slack_client
|
||||
Slack.configure do |config|
|
||||
config.token = hook.access_token
|
||||
end
|
||||
Slack::Web::Client.new
|
||||
end
|
||||
|
||||
def create_channel
|
||||
@channel = slack_client.conversations_create(name: params[:channel])
|
||||
end
|
||||
|
||||
def update_reference_id
|
||||
@hook.reference_id = channel['channel']['id']
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
class Integrations::Slack::HookBuilder
|
||||
attr_reader :params
|
||||
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def perform
|
||||
token = fetch_access_token
|
||||
|
||||
hook = account.hooks.new(
|
||||
access_token: token,
|
||||
status: 'enabled',
|
||||
inbox_id: params[:inbox_id],
|
||||
hook_type: hook_type,
|
||||
app_id: 'cw_slack'
|
||||
)
|
||||
|
||||
hook.save!
|
||||
hook
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def account
|
||||
params[:account]
|
||||
end
|
||||
|
||||
def hook_type
|
||||
params[:inbox_id] ? 'inbox' : 'account'
|
||||
end
|
||||
|
||||
def fetch_access_token
|
||||
client = Slack::Web::Client.new
|
||||
|
||||
client.oauth_access(
|
||||
client_id: ENV.fetch('SLACK_CLIENT_ID', 'TEST_CLIENT_ID'),
|
||||
client_secret: ENV.fetch('SLACK_CLIENT_SECRET', 'TEST_CLIENT_SECRET'),
|
||||
code: params[:code]
|
||||
)['bot']['bot_access_token']
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,78 @@
|
||||
class Integrations::Slack::IncomingMessageBuilder
|
||||
attr_reader :params
|
||||
|
||||
SUPPORTED_EVENT_TYPES = %w[event_callback url_verification].freeze
|
||||
SUPPORTED_EVENTS = %w[message].freeze
|
||||
SUPPORTED_MESSAGE_TYPES = %w[rich_text].freeze
|
||||
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def perform
|
||||
return unless valid_event?
|
||||
|
||||
if hook_verification?
|
||||
verify_hook
|
||||
elsif create_message?
|
||||
create_message
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_event?
|
||||
supported_event_type? && supported_event?
|
||||
end
|
||||
|
||||
def supported_event_type?
|
||||
SUPPORTED_EVENT_TYPES.include?(params[:type])
|
||||
end
|
||||
|
||||
def supported_event?
|
||||
hook_verification? || SUPPORTED_EVENTS.include?(params[:event][:type])
|
||||
end
|
||||
|
||||
def supported_message?
|
||||
SUPPORTED_MESSAGE_TYPES.include?(message[:type])
|
||||
end
|
||||
|
||||
def hook_verification?
|
||||
params[:type] == 'url_verification'
|
||||
end
|
||||
|
||||
def create_message?
|
||||
supported_message? && integration_hook
|
||||
end
|
||||
|
||||
def message
|
||||
params[:event][:blocks].first
|
||||
end
|
||||
|
||||
def verify_hook
|
||||
{
|
||||
challenge: params[:challenge]
|
||||
}
|
||||
end
|
||||
|
||||
def integration_hook
|
||||
@integration_hook ||= Integrations::Hook.where(reference_id: params[:event][:channel])
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Conversation.where(identifier: params[:event][:thread_ts]).first
|
||||
end
|
||||
|
||||
def create_message
|
||||
return unless conversation
|
||||
|
||||
conversation.messages.create(
|
||||
message_type: 0,
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
content: message[:elements].first[:elements].first[:text]
|
||||
)
|
||||
|
||||
{ status: 'success' }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
class Integrations::Slack::OutgoingMessageBuilder
|
||||
attr_reader :hook, :message
|
||||
|
||||
def self.perform(hook, message)
|
||||
new(hook, message).perform
|
||||
end
|
||||
|
||||
def initialize(hook, message)
|
||||
@hook = hook
|
||||
@message = message
|
||||
end
|
||||
|
||||
def perform
|
||||
send_message
|
||||
update_reference_id
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def conversation
|
||||
@conversation ||= message.conversation
|
||||
end
|
||||
|
||||
def contact
|
||||
@contact ||= conversation.contact
|
||||
end
|
||||
|
||||
def send_message
|
||||
@slack_message = slack_client.chat_postMessage(
|
||||
channel: hook.reference_id,
|
||||
text: message.content,
|
||||
username: contact.try(:name),
|
||||
thread_ts: conversation.identifier
|
||||
)
|
||||
end
|
||||
|
||||
def update_reference_id
|
||||
return if conversation.identifier
|
||||
|
||||
conversation.identifier = @slack_message['ts']
|
||||
conversation.save!
|
||||
end
|
||||
|
||||
def slack_client
|
||||
Slack.configure do |config|
|
||||
config.token = hook.access_token
|
||||
end
|
||||
Slack::Web::Client.new
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Integration Apps API', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'GET /api/v1/integrations/apps' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get api_v1_account_integrations_apps_url(account)
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
it 'returns all the apps' do
|
||||
get api_v1_account_integrations_apps_url(account),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
app = JSON.parse(response.body).first
|
||||
expect(app['id']).to eql('cw_slack')
|
||||
expect(app['name']).to eql('Slack')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/integrations/apps/:id' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'cw_slack')
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
it 'returns details of the app' do
|
||||
get api_v1_account_integrations_app_url(account_id: account.id, id: 'cw_slack'),
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
app = JSON.parse(response.body)
|
||||
expect(app['id']).to eql('cw_slack')
|
||||
expect(app['name']).to eql('Slack')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,6 +7,7 @@ FactoryBot.define do
|
||||
user_last_seen_at { Time.current }
|
||||
agent_last_seen_at { Time.current }
|
||||
locked { false }
|
||||
identifier { SecureRandom.hex }
|
||||
|
||||
after(:build) do |conversation|
|
||||
conversation.account ||= create(:account)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
FactoryBot.define do
|
||||
factory :integrations_hook, class: 'Integrations::Hook' do
|
||||
status { 1 }
|
||||
inbox_id { 1 }
|
||||
account_id { 1 }
|
||||
app_id { 'cw_slack' }
|
||||
settings { 'MyText' }
|
||||
hook_type { 1 }
|
||||
access_token { SecureRandom.hex }
|
||||
reference_id { SecureRandom.hex }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe HookJob, type: :job do
|
||||
subject(:job) { described_class.perform_later(hook, message) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
let(:message) { create(:message) }
|
||||
|
||||
it 'queues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(hook, message)
|
||||
.on_queue('integrations')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Integrations::Slack::HookBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:code) { SecureRandom.hex }
|
||||
let(:token) { SecureRandom.hex }
|
||||
|
||||
describe '#perform' do
|
||||
it 'creates hook' do
|
||||
hooks_count = account.hooks.count
|
||||
|
||||
builder = described_class.new(account: account, code: code)
|
||||
builder.stub(:fetch_access_token) { token }
|
||||
|
||||
builder.perform
|
||||
expect(account.hooks.count).to eql(hooks_count + 1)
|
||||
|
||||
hook = account.hooks.last
|
||||
expect(hook.access_token).to eql(token)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Integrations::Slack::IncomingMessageBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let(:message_params) { slack_message_stub }
|
||||
let(:verification_params) { slack_url_verification_stub }
|
||||
|
||||
let(:hook) { create(:integrations_hook, account: account, reference_id: message_params[:event][:channel]) }
|
||||
let!(:conversation) { create(:conversation, identifier: message_params[:event][:thread_ts]) }
|
||||
|
||||
describe '#perform' do
|
||||
context 'when url verification' do
|
||||
it 'return challenge code as response' do
|
||||
builder = described_class.new(verification_params)
|
||||
response = builder.perform
|
||||
expect(response[:challenge]).to eql(verification_params[:challenge])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when message creation' do
|
||||
it 'creates message' do
|
||||
messages_count = conversation.messages.count
|
||||
builder = described_class.new(message_params)
|
||||
builder.perform
|
||||
expect(conversation.messages.count).to eql(messages_count + 1)
|
||||
end
|
||||
|
||||
it 'does not create message for invalid event type' do
|
||||
messages_count = conversation.messages.count
|
||||
message_params[:type] = 'invalid_event_type'
|
||||
builder = described_class.new(message_params)
|
||||
builder.perform
|
||||
expect(conversation.messages.count).to eql(messages_count)
|
||||
end
|
||||
|
||||
it 'does not create message for invalid event name' do
|
||||
messages_count = conversation.messages.count
|
||||
message_params[:event][:type] = 'invalid_event_name'
|
||||
builder = described_class.new(message_params)
|
||||
builder.perform
|
||||
expect(conversation.messages.count).to eql(messages_count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Integrations::Slack::OutgoingMessageBuilder do
|
||||
let(:account) { create(:account) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
let!(:contact) { create(:contact) }
|
||||
|
||||
let!(:hook) { create(:integrations_hook, account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact) }
|
||||
let!(:message) { create(:message, account: account, inbox: inbox, conversation: conversation) }
|
||||
|
||||
describe '#perform' do
|
||||
it 'sent message to slack' do
|
||||
builder = described_class.new(hook, message)
|
||||
stub_request(:post, 'https://slack.com/api/chat.postMessage')
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
|
||||
# rubocop:disable RSpec/AnyInstance
|
||||
allow_any_instance_of(Slack::Web::Client).to receive(:chat_postMessage).with(
|
||||
channel: hook.reference_id,
|
||||
text: message.content,
|
||||
username: contact.name,
|
||||
thread_ts: conversation.identifier
|
||||
)
|
||||
# rubocop:enable RSpec/AnyInstance
|
||||
|
||||
builder.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
require 'rails_helper'
|
||||
describe HookListener do
|
||||
let(:listener) { described_class.instance }
|
||||
let!(:account) { create(:account) }
|
||||
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 hook is not configured' do
|
||||
it 'does not trigger hook job' do
|
||||
expect(HookJob).to receive(:perform_later).exactly(0).times
|
||||
listener.message_created(event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hook is configured' do
|
||||
it 'triggers hook job' do
|
||||
hook = create(:integrations_hook, account: account)
|
||||
expect(HookJob).to receive(:perform_later).with(hook, message).once
|
||||
listener.message_created(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,6 +29,8 @@ RSpec.describe Inbox do
|
||||
it { is_expected.to have_many(:webhooks).dependent(:destroy) }
|
||||
|
||||
it { is_expected.to have_many(:events) }
|
||||
|
||||
it { is_expected.to have_many(:hooks) }
|
||||
end
|
||||
|
||||
describe '#add_member' do
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Integrations::Hook, type: :model do
|
||||
context 'with validations' do
|
||||
it { is_expected.to validate_presence_of(:app_id) }
|
||||
it { is_expected.to validate_presence_of(:account_id) }
|
||||
end
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:account) }
|
||||
end
|
||||
end
|
||||
@@ -22,7 +22,9 @@ require 'sidekiq/testing'
|
||||
# directory. Alternatively, in the individual `*_spec.rb` files, manually
|
||||
# require only the support files necessary.
|
||||
#
|
||||
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
|
||||
# rubocop:disable Rails/FilePath
|
||||
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
|
||||
# rubocop:enable Rails/FilePath
|
||||
|
||||
# Checks for pending migrations and applies them before tests are run.
|
||||
# If you are not using ActiveRecord, you can remove these lines.
|
||||
@@ -61,7 +63,7 @@ RSpec.configure do |config|
|
||||
config.filter_rails_from_backtrace!
|
||||
# arbitrary gems may also be filtered via:
|
||||
# config.filter_gems_from_backtrace("gem name")
|
||||
|
||||
config.include SlackStubs
|
||||
config.include Devise::Test::IntegrationHelpers, type: :request
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Accounts::Integrations::Slacks', type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:hook) { create(:integrations_hook, account: account) }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/integrations/slack' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/slack", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'creates hook' do
|
||||
hook_builder = Integrations::Slack::HookBuilder.new(account: account, code: SecureRandom.hex)
|
||||
hook_builder.stub(:fetch_access_token) { SecureRandom.hex }
|
||||
|
||||
expect(Integrations::Slack::HookBuilder).to receive(:new).and_return(hook_builder)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/integrations/slack",
|
||||
params: { code: SecureRandom.hex },
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['app_id']).to eql('cw_slack')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/accounts/{account.id}/integrations/slack/{id}' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'updates hook' do
|
||||
channel_builder = Integrations::Slack::ChannelBuilder.new(hook: hook, channel: 'channel')
|
||||
channel_builder.stub(:perform)
|
||||
|
||||
expect(Integrations::Slack::ChannelBuilder).to receive(:new).and_return(channel_builder)
|
||||
|
||||
put "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}",
|
||||
params: { channel: SecureRandom.hex },
|
||||
headers: agent.create_new_auth_token
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = JSON.parse(response.body)
|
||||
expect(json_response['app_id']).to eql('cw_slack')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/integrations/slack/{id}' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}", params: {}
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
it 'deletes hook' do
|
||||
delete "/api/v1/accounts/#{account.id}/integrations/slack/#{hook.id}",
|
||||
headers: agent.create_new_auth_token
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(Integrations::Hook.find_by(id: hook.id)).to be nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Integrations::Webhooks', type: :request do
|
||||
describe 'POST /api/v1/integrations/webhooks' do
|
||||
it 'consumes webhook' do
|
||||
builder = Integrations::Slack::IncomingMessageBuilder.new({})
|
||||
builder.stub(:perform) { true }
|
||||
|
||||
expect(Integrations::Slack::IncomingMessageBuilder).to receive(:new).and_return(builder)
|
||||
|
||||
post '/api/v1/integrations/webhooks', params: {}
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,8 @@
|
||||
require 'simplecov'
|
||||
require 'webmock/rspec'
|
||||
|
||||
SimpleCov.start 'rails'
|
||||
WebMock.allow_net_connect!
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.expect_with :rspec do |expectations|
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
module SlackStubs
|
||||
def slack_url_verification_stub
|
||||
{
|
||||
"token": 'Jhj5dZrVaK7ZwHHjRyZWjbDl',
|
||||
"challenge": '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P',
|
||||
"type": 'url_verification'
|
||||
}
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def slack_message_stub
|
||||
{
|
||||
"token": '[FILTERED]',
|
||||
"team_id": 'TLST3048H',
|
||||
"api_app_id": 'A012S5UETV4',
|
||||
"event": {
|
||||
"client_msg_id": 'ffc6e64e-6f0c-4a3d-b594-faa6b44e48ab',
|
||||
"type": 'message',
|
||||
"text": 'this is test',
|
||||
"user": 'ULYPAKE5S',
|
||||
"ts": '1588623033.006000',
|
||||
"team": 'TLST3048H',
|
||||
"blocks": [
|
||||
{
|
||||
"type": 'rich_text',
|
||||
"block_id": 'jaIv3',
|
||||
"elements": [
|
||||
{
|
||||
"type": 'rich_text_section',
|
||||
"elements": [
|
||||
{
|
||||
"type": 'text',
|
||||
"text": 'this is test'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"thread_ts": '1588623023.005900',
|
||||
"channel": 'G01354F6A6Q',
|
||||
"event_ts": '1588623033.006000',
|
||||
"channel_type": 'group'
|
||||
},
|
||||
"type": 'event_callback',
|
||||
"event_id": 'Ev013QUX3WV6',
|
||||
"event_time": 1_588_623_033,
|
||||
"authed_users": '[FILTERED]',
|
||||
"webhook": {}
|
||||
}
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
end
|
||||
Reference in New Issue
Block a user