Chore: Routine Bugfixes and enhancements (#979)
- Fix slack scopes - Docs for authentication Fixes: #704 , #973
This commit is contained in:
parent
0aab717bb3
commit
4f83d5451e
32 changed files with 254 additions and 147 deletions
|
@ -98,7 +98,7 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
name: Run backend tests
|
name: Run backend tests
|
||||||
command: |
|
command: |
|
||||||
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile=10
|
||||||
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
~/tmp/cc-test-reporter format-coverage -t simplecov -o ~/tmp/codeclimate.backend.json coverage/backend/.resultset.json
|
||||||
- persist_to_workspace:
|
- persist_to_workspace:
|
||||||
root: ~/tmp
|
root: ~/tmp
|
||||||
|
|
36
Gemfile.lock
36
Gemfile.lock
|
@ -93,10 +93,10 @@ GEM
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
ast (2.4.1)
|
ast (2.4.1)
|
||||||
attr_extras (6.2.4)
|
attr_extras (6.2.4)
|
||||||
autoprefixer-rails (9.7.6)
|
autoprefixer-rails (9.8.2)
|
||||||
execjs
|
execjs
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.329.0)
|
aws-partitions (1.332.0)
|
||||||
aws-sdk-core (3.100.0)
|
aws-sdk-core (3.100.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
|
@ -105,12 +105,12 @@ GEM
|
||||||
aws-sdk-kms (1.34.1)
|
aws-sdk-kms (1.34.1)
|
||||||
aws-sdk-core (~> 3, >= 3.99.0)
|
aws-sdk-core (~> 3, >= 3.99.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.68.1)
|
aws-sdk-s3 (1.69.1)
|
||||||
aws-sdk-core (~> 3, >= 3.99.0)
|
aws-sdk-core (~> 3, >= 3.99.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.1.4)
|
aws-sigv4 (1.2.0)
|
||||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
axiom-types (0.1.1)
|
axiom-types (0.1.1)
|
||||||
descendants_tracker (~> 0.0.4)
|
descendants_tracker (~> 0.0.4)
|
||||||
ice_nine (~> 0.11.0)
|
ice_nine (~> 0.11.0)
|
||||||
|
@ -163,7 +163,7 @@ GEM
|
||||||
devise (> 3.5.2, < 5)
|
devise (> 3.5.2, < 5)
|
||||||
rails (>= 4.2.0, < 6.1)
|
rails (>= 4.2.0, < 6.1)
|
||||||
sprockets (= 3.7.2)
|
sprockets (= 3.7.2)
|
||||||
diff-lcs (1.3)
|
diff-lcs (1.4)
|
||||||
digest-crc (0.5.1)
|
digest-crc (0.5.1)
|
||||||
docile (1.3.2)
|
docile (1.3.2)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
|
@ -178,11 +178,11 @@ GEM
|
||||||
facebook-messenger (1.5.0)
|
facebook-messenger (1.5.0)
|
||||||
httparty (~> 0.13, >= 0.13.7)
|
httparty (~> 0.13, >= 0.13.7)
|
||||||
rack (>= 1.4.5)
|
rack (>= 1.4.5)
|
||||||
factory_bot (5.2.0)
|
factory_bot (6.0.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (5.2.0)
|
factory_bot_rails (6.0.0)
|
||||||
factory_bot (~> 5.2.0)
|
factory_bot (~> 6.0.0)
|
||||||
railties (>= 4.2.0)
|
railties (>= 5.0.0)
|
||||||
faker (2.12.0)
|
faker (2.12.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (1.0.1)
|
faraday (1.0.1)
|
||||||
|
@ -197,7 +197,7 @@ GEM
|
||||||
gli (2.19.1)
|
gli (2.19.1)
|
||||||
globalid (0.4.2)
|
globalid (0.4.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
google-api-client (0.40.2)
|
google-api-client (0.41.0)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (~> 0.9)
|
googleauth (~> 0.9)
|
||||||
httpclient (>= 2.8.1, < 3.0)
|
httpclient (>= 2.8.1, < 3.0)
|
||||||
|
@ -218,7 +218,7 @@ GEM
|
||||||
google-cloud-core (~> 1.2)
|
google-cloud-core (~> 1.2)
|
||||||
googleauth (~> 0.9)
|
googleauth (~> 0.9)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (0.12.0)
|
googleauth (0.13.0)
|
||||||
faraday (>= 0.17.3, < 2.0)
|
faraday (>= 0.17.3, < 2.0)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
memoist (~> 0.16)
|
||||||
|
@ -306,8 +306,8 @@ GEM
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.0)
|
os (1.1.0)
|
||||||
parallel (1.19.2)
|
parallel (1.19.2)
|
||||||
parser (2.7.1.3)
|
parser (2.7.1.4)
|
||||||
ast (~> 2.4.0)
|
ast (~> 2.4.1)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
pry (0.13.1)
|
pry (0.13.1)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
|
@ -367,7 +367,7 @@ GEM
|
||||||
redis-rack-cache (2.2.1)
|
redis-rack-cache (2.2.1)
|
||||||
rack-cache (>= 1.10, < 2)
|
rack-cache (>= 1.10, < 2)
|
||||||
redis-store (>= 1.6, < 2)
|
redis-store (>= 1.6, < 2)
|
||||||
redis-store (1.8.2)
|
redis-store (1.9.0)
|
||||||
redis (>= 4, < 5)
|
redis (>= 4, < 5)
|
||||||
regexp_parser (1.7.1)
|
regexp_parser (1.7.1)
|
||||||
representable (3.0.4)
|
representable (3.0.4)
|
||||||
|
@ -401,13 +401,13 @@ GEM
|
||||||
rspec-mocks (~> 3.9)
|
rspec-mocks (~> 3.9)
|
||||||
rspec-support (~> 3.9)
|
rspec-support (~> 3.9)
|
||||||
rspec-support (3.9.3)
|
rspec-support (3.9.3)
|
||||||
rubocop (0.85.1)
|
rubocop (0.86.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.7.0.1)
|
parser (>= 2.7.0.1)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.7)
|
regexp_parser (>= 1.7)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 0.0.3)
|
rubocop-ast (>= 0.0.3, < 1.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 2.0)
|
unicode-display_width (>= 1.4.0, < 2.0)
|
||||||
rubocop-ast (0.0.3)
|
rubocop-ast (0.0.3)
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Api::V1::Accounts::LabelsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@label.update(permitted_params)
|
@label.update!(permitted_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
|
|
@ -59,11 +59,19 @@ class ApplicationController < ActionController::Base
|
||||||
render json: exception.to_hash, status: exception.http_status
|
render json: exception.to_hash, status: exception.http_status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def locale_from_params
|
||||||
|
I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def locale_from_account(account)
|
||||||
|
I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil
|
||||||
|
end
|
||||||
|
|
||||||
def switch_locale(account)
|
def switch_locale(account)
|
||||||
# priority is for locale set in query string (mostly for widget/from js sdk)
|
# priority is for locale set in query string (mostly for widget/from js sdk)
|
||||||
locale ||= (I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil)
|
locale ||= locale_from_params
|
||||||
# if local is not set in param, lets try account
|
# if local is not set in param, lets try account
|
||||||
locale ||= (I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil)
|
locale ||= locale_from_account(account)
|
||||||
I18n.locale = locale || I18n.default_locale
|
I18n.locale = locale || I18n.default_locale
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ class WidgetsController < ActionController::Base
|
||||||
before_action :set_token
|
before_action :set_token
|
||||||
before_action :set_contact
|
before_action :set_contact
|
||||||
before_action :build_contact
|
before_action :build_contact
|
||||||
|
after_action :allow_iframe_requests
|
||||||
|
|
||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
|
@ -50,4 +51,8 @@ class WidgetsController < ActionController::Base
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:website_token, :cw_conversation)
|
params.permit(:website_token, :cw_conversation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def allow_iframe_requests
|
||||||
|
response.headers.delete('X-Frame-Options')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -62,7 +62,7 @@ class ConversationFinder
|
||||||
|
|
||||||
def find_all_conversations
|
def find_all_conversations
|
||||||
@conversations = current_account.conversations.includes(
|
@conversations = current_account.conversations.includes(
|
||||||
:assignee, :contact, :inbox
|
:assignee, :inbox, contact: [:avatar_attachment]
|
||||||
).where(inbox_id: @inbox_ids)
|
).where(inbox_id: @inbox_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { required, minLength } from 'vuelidate/lib/validators';
|
import { required, minLength } from 'vuelidate/lib/validators';
|
||||||
|
|
||||||
export const validLabelCharacters = (str = '') => /^[\w-_]+$/g.test(str);
|
export const validLabelCharacters = (str = '') => !!str && !str.includes(' ');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: {
|
title: {
|
||||||
|
|
|
@ -4,7 +4,7 @@ class HookJob < ApplicationJob
|
||||||
def perform(hook, message)
|
def perform(hook, message)
|
||||||
return unless hook.slack?
|
return unless hook.slack?
|
||||||
|
|
||||||
Integrations::Slack::OutgoingMessageBuilder.perform(hook, message)
|
Integrations::Slack::SendOnSlackService.new(message: message, hook: hook).perform
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Raven.capture_exception(e)
|
Raven.capture_exception(e)
|
||||||
end
|
end
|
||||||
|
|
|
@ -138,11 +138,11 @@ class Message < ApplicationRecord
|
||||||
def send_reply
|
def send_reply
|
||||||
channel_name = conversation.inbox.channel.class.to_s
|
channel_name = conversation.inbox.channel.class.to_s
|
||||||
if channel_name == 'Channel::FacebookPage'
|
if channel_name == 'Channel::FacebookPage'
|
||||||
::Facebook::SendReplyService.new(message: self).perform
|
::Facebook::SendOnFacebookService.new(message: self).perform
|
||||||
elsif channel_name == 'Channel::TwitterProfile'
|
elsif channel_name == 'Channel::TwitterProfile'
|
||||||
::Twitter::SendReplyService.new(message: self).perform
|
::Twitter::SendOnTwitterService.new(message: self).perform
|
||||||
elsif channel_name == 'Channel::TwilioSms'
|
elsif channel_name == 'Channel::TwilioSms'
|
||||||
::Twilio::OutgoingMessageService.new(message: self).perform
|
::Twilio::SendOnTwilioService.new(message: self).perform
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
55
app/services/base/send_on_channel_service.rb
Normal file
55
app/services/base/send_on_channel_service.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
#######################################
|
||||||
|
# To create an external channel reply service
|
||||||
|
# - Inherit this as the base class.
|
||||||
|
# - Implement `channel_class` method in your child class.
|
||||||
|
# - Implement `perform_reply` method in your child class.
|
||||||
|
# - Implement additional custom logic for your `perform_reply` method.
|
||||||
|
# - When required override the validation_methods.
|
||||||
|
# - Use Childclass.new.perform.
|
||||||
|
######################################
|
||||||
|
class Base::SendOnChannelService
|
||||||
|
pattr_initialize [:message!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
validate_target_channel
|
||||||
|
return unless outgoing_message?
|
||||||
|
return if invalid_message?
|
||||||
|
|
||||||
|
perform_reply
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
delegate :conversation, to: :message
|
||||||
|
delegate :contact, :contact_inbox, :inbox, to: :conversation
|
||||||
|
delegate :channel, to: :inbox
|
||||||
|
|
||||||
|
def channel_class
|
||||||
|
raise 'Overwrite this method in child class'
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_reply
|
||||||
|
raise 'Overwrite this method in child class'
|
||||||
|
end
|
||||||
|
|
||||||
|
def outgoing_message_originated_from_channel?
|
||||||
|
# TODO: we need to refactor this logic as more integrations comes by
|
||||||
|
# chatwoot messages won't have source id at the moment
|
||||||
|
# outgoing messages may be created in slack which should be send to the channel
|
||||||
|
message.source_id.present? && !message.source_id.starts_with?('slack_')
|
||||||
|
end
|
||||||
|
|
||||||
|
def outgoing_message?
|
||||||
|
message.outgoing? || message.template?
|
||||||
|
end
|
||||||
|
|
||||||
|
def invalid_message?
|
||||||
|
# private notes aren't send to the channels
|
||||||
|
# we should also avoid the case of message loops, when outgoing messages are created from channel
|
||||||
|
message.private? || outgoing_message_originated_from_channel?
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_target_channel
|
||||||
|
raise 'Invalid channel service was called' if inbox.channel.class != channel_class
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,37 +1,14 @@
|
||||||
class Facebook::SendReplyService
|
class Facebook::SendOnFacebookService < Base::SendOnChannelService
|
||||||
pattr_initialize [:message!]
|
|
||||||
|
|
||||||
def perform
|
|
||||||
return if message.private
|
|
||||||
return if inbox.channel.class.to_s != 'Channel::FacebookPage'
|
|
||||||
return unless outgoing_message_from_chatwoot?
|
|
||||||
|
|
||||||
FacebookBot::Bot.deliver(delivery_params, access_token: message.channel_token)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
delegate :contact, to: :conversation
|
def channel_class
|
||||||
|
Channel::FacebookPage
|
||||||
def inbox
|
|
||||||
@inbox ||= message.inbox
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation
|
def perform_reply
|
||||||
@conversation ||= message.conversation
|
FacebookBot::Bot.deliver(delivery_params, access_token: message.channel_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
def outgoing_message_from_chatwoot?
|
|
||||||
# messages sent directly from chatwoot won't have source_id.
|
|
||||||
(message.outgoing? || message.template?) && !message.source_id
|
|
||||||
end
|
|
||||||
|
|
||||||
# def reopen_lock
|
|
||||||
# if message.incoming? && conversation.locked?
|
|
||||||
# conversation.unlock!
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
|
|
||||||
def fb_text_message_params
|
def fb_text_message_params
|
||||||
{
|
{
|
||||||
recipient: { id: contact.get_source_id(inbox.id) },
|
recipient: { id: contact.get_source_id(inbox.id) },
|
|
@ -1,22 +1,15 @@
|
||||||
class Twilio::OutgoingMessageService
|
class Twilio::SendOnTwilioService < Base::SendOnChannelService
|
||||||
pattr_initialize [:message!]
|
private
|
||||||
|
|
||||||
def perform
|
def channel_class
|
||||||
return if message.private
|
Channel::TwilioSms
|
||||||
return if message.source_id
|
end
|
||||||
return if inbox.channel.class.to_s != 'Channel::TwilioSms'
|
|
||||||
return unless outgoing_message?
|
|
||||||
|
|
||||||
|
def perform_reply
|
||||||
twilio_message = client.messages.create(message_params)
|
twilio_message = client.messages.create(message_params)
|
||||||
message.update!(source_id: twilio_message.sid)
|
message.update!(source_id: twilio_message.sid)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
delegate :conversation, to: :message
|
|
||||||
delegate :contact, to: :conversation
|
|
||||||
delegate :contact_inbox, to: :conversation
|
|
||||||
|
|
||||||
def message_params
|
def message_params
|
||||||
params = {
|
params = {
|
||||||
body: message.content,
|
body: message.content,
|
|
@ -1,16 +1,17 @@
|
||||||
class Twitter::SendReplyService
|
class Twitter::SendOnTwitterService < Base::SendOnChannelService
|
||||||
pattr_initialize [:message!]
|
pattr_initialize [:message!]
|
||||||
|
|
||||||
def perform
|
private
|
||||||
return if message.private
|
|
||||||
return if message.source_id
|
|
||||||
return if inbox.channel.class.to_s != 'Channel::TwitterProfile'
|
|
||||||
return unless outgoing_message_from_chatwoot?
|
|
||||||
|
|
||||||
send_reply
|
delegate :additional_attributes, to: :contact
|
||||||
|
|
||||||
|
def channel_class
|
||||||
|
Channel::TwitterProfile
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def perform_reply
|
||||||
|
conversation_type == 'tweet' ? send_tweet_reply : send_direct_message
|
||||||
|
end
|
||||||
|
|
||||||
def twitter_client
|
def twitter_client
|
||||||
Twitty::Facade.new do |config|
|
Twitty::Facade.new do |config|
|
||||||
|
@ -50,19 +51,4 @@ class Twitter::SendReplyService
|
||||||
Rails.logger.info 'TWITTER_TWEET_REPLY_ERROR' + response.body
|
Rails.logger.info 'TWITTER_TWEET_REPLY_ERROR' + response.body
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_reply
|
|
||||||
conversation_type == 'tweet' ? send_tweet_reply : send_direct_message
|
|
||||||
end
|
|
||||||
|
|
||||||
def outgoing_message_from_chatwoot?
|
|
||||||
(message.outgoing? || message.template?)
|
|
||||||
end
|
|
||||||
|
|
||||||
delegate :additional_attributes, to: :contact
|
|
||||||
delegate :contact, to: :conversation
|
|
||||||
delegate :contact_inbox, to: :conversation
|
|
||||||
delegate :conversation, to: :message
|
|
||||||
delegate :inbox, to: :conversation
|
|
||||||
delegate :channel, to: :inbox
|
|
||||||
end
|
end
|
|
@ -12,7 +12,7 @@ json.id conversation.display_id
|
||||||
if conversation.unread_incoming_messages.count.zero?
|
if conversation.unread_incoming_messages.count.zero?
|
||||||
json.messages [conversation.messages.last.try(:push_event_data)]
|
json.messages [conversation.messages.last.try(:push_event_data)]
|
||||||
else
|
else
|
||||||
json.messages conversation.unread_messages.map(&:push_event_data)
|
json.messages conversation.unread_messages.includes([:user, :attachments]).map(&:push_event_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
json.inbox_id conversation.inbox_id
|
json.inbox_id conversation.inbox_id
|
||||||
|
|
|
@ -27,10 +27,6 @@ module Chatwoot
|
||||||
config.generators.javascripts = false
|
config.generators.javascripts = false
|
||||||
config.generators.stylesheets = false
|
config.generators.stylesheets = false
|
||||||
|
|
||||||
config.action_dispatch.default_headers = {
|
|
||||||
'X-Frame-Options' => 'ALLOWALL'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Custom chatwoot configurations
|
# Custom chatwoot configurations
|
||||||
config.x = config_for(:app).with_indifferent_access
|
config.x = config_for(:app).with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,3 +7,7 @@ redis = Rails.env.test? ? MockRedis.new : Redis.new(app_redis_config)
|
||||||
# Alfred - Used currently for round robin and conversation emails.
|
# Alfred - Used currently for round robin and conversation emails.
|
||||||
# Add here as you use it for more features
|
# Add here as you use it for more features
|
||||||
$alfred = Redis::Namespace.new('alfred', redis: redis, warning: true)
|
$alfred = Redis::Namespace.new('alfred', redis: redis, warning: true)
|
||||||
|
|
||||||
|
# https://github.com/mperham/sidekiq/issues/4591
|
||||||
|
# TODO once sidekiq remove we can remove this
|
||||||
|
Redis.exists_returns_integer = false
|
||||||
|
|
|
@ -3,7 +3,7 @@ slack:
|
||||||
name: Slack
|
name: Slack
|
||||||
logo: slack.png
|
logo: slack.png
|
||||||
description: "Slack is a chat tool that brings all your communication together in one place. By integrating Slack, you can get notified of all the new conversations in your account right inside your Slack."
|
description: "Slack is a chat tool that brings all your communication together in one place. By integrating Slack, you can get notified of all the new conversations in your account right inside your Slack."
|
||||||
action: https://slack.com/oauth/v2/authorize?scope=commands,chat:write,channels:manage,channels:join,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize
|
action: https://slack.com/oauth/v2/authorize?scope=commands,chat:write,channels:read,channels:manage,channels:join,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize,channels:history,groups:history,mpim:history,im:history
|
||||||
webhooks:
|
webhooks:
|
||||||
id: webhook
|
id: webhook
|
||||||
name: Webhooks
|
name: Webhooks
|
||||||
|
|
|
@ -16,7 +16,7 @@ Once you register your Slack App, you will have to obtain the `Client Id` and `C
|
||||||
3) Head over to the `OAuth & permissions` section under `features` tab.
|
3) Head over to the `OAuth & permissions` section under `features` tab.
|
||||||
4) In the redirect URLs, Add your Chatwoot installation base url.
|
4) In the redirect URLs, Add your Chatwoot installation base url.
|
||||||
5) In the scopes section configure the given scopes for bot token scopes.
|
5) In the scopes section configure the given scopes for bot token scopes.
|
||||||
`commands,chat:write,channels:manage,channels:join,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize`
|
`commands,chat:write,channels:read,channels:manage,channels:join,groups:write,im:write,mpim:write,users:read,users:read.email,chat:write.customize,channels:history,groups:history,mpim:history,im:history`
|
||||||
6) Head over to the `events subscriptions` section under `features` tab.
|
6) Head over to the `events subscriptions` section under `features` tab.
|
||||||
7) Enable events and configure the the given request url `{chatwoot installation url}/api/v1/integrations/webhooks`
|
7) Enable events and configure the the given request url `{chatwoot installation url}/api/v1/integrations/webhooks`
|
||||||
8) Subscribe to the following bot events `message.channels` , `message.groups`, `message.im`, `message.mpim`
|
8) Subscribe to the following bot events `message.channels` , `message.groups`, `message.im`, `message.mpim`
|
||||||
|
|
|
@ -17,10 +17,7 @@ class Integrations::Slack::ChannelBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def slack_client
|
def slack_client
|
||||||
Slack.configure do |config|
|
@slack_client ||= Slack::Web::Client.new(token: hook.access_token)
|
||||||
config.token = hook.access_token
|
|
||||||
end
|
|
||||||
Slack::Web::Client.new
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_channel
|
def find_or_create_channel
|
||||||
|
@ -29,6 +26,7 @@ class Integrations::Slack::ChannelBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_reference_id
|
def update_reference_id
|
||||||
@hook.update(reference_id: channel['id'])
|
slack_client.conversations_join(channel: channel[:id])
|
||||||
|
@hook.update(reference_id: channel[:id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -89,9 +89,6 @@ class Integrations::Slack::IncomingMessageBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def slack_client
|
def slack_client
|
||||||
Slack.configure do |config|
|
@slack_client ||= Slack::Web::Client.new(token: @integration_hook.access_token)
|
||||||
config.token = integration_hook.access_token
|
|
||||||
end
|
|
||||||
Slack::Web::Client.new
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,30 +1,23 @@
|
||||||
class Integrations::Slack::OutgoingMessageBuilder
|
class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
|
||||||
attr_reader :hook, :message
|
pattr_initialize [:message!, :hook!]
|
||||||
|
|
||||||
def self.perform(hook, message)
|
|
||||||
new(hook, message).perform
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(hook, message)
|
|
||||||
@hook = hook
|
|
||||||
@message = message
|
|
||||||
end
|
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
return if message.source_id.present?
|
# overriding the base class logic since the validations are different in this case.
|
||||||
|
# FIXME: for now we will only send messages from widget to slack
|
||||||
|
return unless channel.is_a?(Channel::WebWidget)
|
||||||
|
# we don't want message loop in slack
|
||||||
|
return if message.source_id.try(:starts_with?, 'slack_')
|
||||||
|
# we don't want to start slack thread from agent conversation as of now
|
||||||
|
return if message.outgoing? && conversation.identifier.blank?
|
||||||
|
|
||||||
send_message
|
perform_reply
|
||||||
update_reference_id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def conversation
|
def perform_reply
|
||||||
@conversation ||= message.conversation
|
send_message
|
||||||
end
|
update_reference_id
|
||||||
|
|
||||||
def contact
|
|
||||||
@contact ||= conversation.contact
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def agent
|
def agent
|
||||||
|
@ -32,8 +25,9 @@ class Integrations::Slack::OutgoingMessageBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_content
|
def message_content
|
||||||
|
private_indicator = message.private? ? 'private: ' : ''
|
||||||
if conversation.identifier.present?
|
if conversation.identifier.present?
|
||||||
message.content
|
"#{private_indicator}#{message.content}"
|
||||||
else
|
else
|
||||||
"*Inbox: #{message.inbox.name}* \n\n #{message.content}"
|
"*Inbox: #{message.inbox.name}* \n\n #{message.content}"
|
||||||
end
|
end
|
||||||
|
@ -59,14 +53,10 @@ class Integrations::Slack::OutgoingMessageBuilder
|
||||||
def update_reference_id
|
def update_reference_id
|
||||||
return if conversation.identifier
|
return if conversation.identifier
|
||||||
|
|
||||||
conversation.identifier = @slack_message['ts']
|
conversation.update!(identifier: @slack_message['ts'])
|
||||||
conversation.save!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def slack_client
|
def slack_client
|
||||||
Slack.configure do |config|
|
@slack_client ||= Slack::Web::Client.new(token: hook.access_token)
|
||||||
config.token = hook.access_token
|
|
||||||
end
|
|
||||||
Slack::Web::Client.new
|
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -14,15 +14,16 @@ RSpec.describe 'Integration Apps API', type: :request do
|
||||||
context 'when it is an authenticated user' do
|
context 'when it is an authenticated user' do
|
||||||
let(:agent) { create(:user, account: account, role: :agent) }
|
let(:agent) { create(:user, account: account, role: :agent) }
|
||||||
|
|
||||||
it 'returns all the apps' do
|
it 'returns all active apps' do
|
||||||
|
first_app = Integrations::App.all.find(&:active?)
|
||||||
get api_v1_account_integrations_apps_url(account),
|
get api_v1_account_integrations_apps_url(account),
|
||||||
headers: agent.create_new_auth_token,
|
headers: agent.create_new_auth_token,
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
app = JSON.parse(response.body)['payload'].first
|
apps = JSON.parse(response.body)['payload'].first
|
||||||
expect(app['id']).to eql('webhook')
|
expect(apps['id']).to eql(first_app.id)
|
||||||
expect(app['name']).to eql('Webhooks')
|
expect(apps['name']).to eql(first_app.name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe Integrations::Slack::OutgoingMessageBuilder do
|
describe Integrations::Slack::SendOnSlackService do
|
||||||
let(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
let!(:inbox) { create(:inbox, account: account) }
|
let!(:inbox) { create(:inbox, account: account) }
|
||||||
let!(:contact) { create(:contact) }
|
let!(:contact) { create(:contact) }
|
||||||
|
@ -11,7 +11,7 @@ describe Integrations::Slack::OutgoingMessageBuilder do
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
it 'sent message to slack' do
|
it 'sent message to slack' do
|
||||||
builder = described_class.new(hook, message)
|
builder = described_class.new(message: message, hook: hook)
|
||||||
stub_request(:post, 'https://slack.com/api/chat.postMessage')
|
stub_request(:post, 'https://slack.com/api/chat.postMessage')
|
||||||
.to_return(status: 200, body: '', headers: {})
|
.to_return(status: 200, body: '', headers: {})
|
||||||
slack_client = double
|
slack_client = double
|
|
@ -1,6 +1,6 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe Facebook::SendReplyService do
|
describe Facebook::SendOnFacebookService do
|
||||||
subject(:send_reply_service) { described_class.new(message: message) }
|
subject(:send_reply_service) { described_class.new(message: message) }
|
||||||
|
|
||||||
before do
|
before do
|
|
@ -1,6 +1,6 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe Twilio::OutgoingMessageService do
|
describe Twilio::SendOnTwilioService do
|
||||||
subject(:outgoing_message_service) { described_class.new(message: message) }
|
subject(:outgoing_message_service) { described_class.new(message: message) }
|
||||||
|
|
||||||
let(:twilio_client) { instance_double(::Twilio::REST::Client) }
|
let(:twilio_client) { instance_double(::Twilio::REST::Client) }
|
|
@ -1,6 +1,6 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
describe Twitter::SendReplyService do
|
describe Twitter::SendOnTwitterService do
|
||||||
subject(:send_reply_service) { described_class.new(message: message) }
|
subject(:send_reply_service) { described_class.new(message: message) }
|
||||||
|
|
||||||
let(:twitter_client) { instance_double(::Twitty::Facade) }
|
let(:twitter_client) { instance_double(::Twitty::Facade) }
|
|
@ -14,6 +14,4 @@ RSpec.configure do |config|
|
||||||
end
|
end
|
||||||
|
|
||||||
config.shared_context_metadata_behavior = :apply_to_host_groups
|
config.shared_context_metadata_behavior = :apply_to_host_groups
|
||||||
|
|
||||||
# config.include Rails.application.routes.url_helpers
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,6 +17,24 @@ produces:
|
||||||
- application/json; charset=utf-8
|
- application/json; charset=utf-8
|
||||||
consumes:
|
consumes:
|
||||||
- application/json; charset=utf-8
|
- application/json; charset=utf-8
|
||||||
|
securityDefinitions:
|
||||||
|
userApiKey:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: api_access_token
|
||||||
|
description: This token can be obtained by visiting the profile page or via rails console. Provides access to endpoints based on the user permissions levels. This token can be saved by an external system when user is created via API, to perform activities on behalf of the user.
|
||||||
|
agentBotApiKey:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: api_access_token
|
||||||
|
description: This token should be provided by system admin or obtained via rails console. This token can be used to build bot integrations and can only access limited apis.
|
||||||
|
superAdminApiKey:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: api_access_token
|
||||||
|
description: This token is only for the system admin or obtained via rails console. This token is to be used rarely for cases like creating a pre verified user through api from external system.
|
||||||
|
security:
|
||||||
|
- userApiKey: []
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
$ref: ./paths/index.yml
|
$ref: ./paths/index.yml
|
||||||
|
|
|
@ -45,6 +45,9 @@ post:
|
||||||
operationId: newConversation
|
operationId: newConversation
|
||||||
summary: Create New Conversation
|
summary: Create New Conversation
|
||||||
description: Create conversation
|
description: Create conversation
|
||||||
|
security:
|
||||||
|
- userApiKey: []
|
||||||
|
- agentBotApiKey: []
|
||||||
parameters:
|
parameters:
|
||||||
- name: data
|
- name: data
|
||||||
in: body
|
in: body
|
||||||
|
|
|
@ -30,6 +30,9 @@ post:
|
||||||
operationId: conversationNewMessage
|
operationId: conversationNewMessage
|
||||||
summary: Create New Message
|
summary: Create New Message
|
||||||
description: All the agent replies are created as new messages through this endpoint
|
description: All the agent replies are created as new messages through this endpoint
|
||||||
|
security:
|
||||||
|
- userApiKey: []
|
||||||
|
- agentBotApiKey: []
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
|
|
|
@ -4,6 +4,9 @@ post:
|
||||||
operationId: conversationToggleStatus
|
operationId: conversationToggleStatus
|
||||||
summary: Toggle Status
|
summary: Toggle Status
|
||||||
description: Toggles the status of the conversation between open and resolved
|
description: Toggles the status of the conversation between open and resolved
|
||||||
|
security:
|
||||||
|
- userApiKey: []
|
||||||
|
- agentBotApiKey: []
|
||||||
parameters:
|
parameters:
|
||||||
- name: id
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
|
|
|
@ -24,6 +24,33 @@
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json; charset=utf-8"
|
"application/json; charset=utf-8"
|
||||||
],
|
],
|
||||||
|
"securityDefinitions": {
|
||||||
|
"userApiKey": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"in": "header",
|
||||||
|
"name": "api_access_token",
|
||||||
|
"description": "This token can be obtained by visiting the profile page or via rails console. Provides access to endpoints based on the user permissions levels. This token can be saved by an external system when user is created via API, to perform activities on behalf of the user."
|
||||||
|
},
|
||||||
|
"agentBotApiKey": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"in": "header",
|
||||||
|
"name": "api_access_token",
|
||||||
|
"description": "This token should be provided by system admin or obtained via rails console. This token can be used to build bot integrations and can only access limited apis."
|
||||||
|
},
|
||||||
|
"superAdminApiKey": {
|
||||||
|
"type": "apiKey",
|
||||||
|
"in": "header",
|
||||||
|
"name": "api_access_token",
|
||||||
|
"description": "This token is only for the system admin or obtained via rails console. This token is to be used rarely for cases like creating a pre verified user through api from external system."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"userApiKey": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"/accounts/{account_id}/inboxes": {
|
"/accounts/{account_id}/inboxes": {
|
||||||
"post": {
|
"post": {
|
||||||
|
@ -325,6 +352,18 @@
|
||||||
"operationId": "newConversation",
|
"operationId": "newConversation",
|
||||||
"summary": "Create New Conversation",
|
"summary": "Create New Conversation",
|
||||||
"description": "Create conversation",
|
"description": "Create conversation",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"userApiKey": [
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agentBotApiKey": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "data",
|
"name": "data",
|
||||||
|
@ -409,6 +448,18 @@
|
||||||
"operationId": "conversationToggleStatus",
|
"operationId": "conversationToggleStatus",
|
||||||
"summary": "Toggle Status",
|
"summary": "Toggle Status",
|
||||||
"description": "Toggles the status of the conversation between open and resolved",
|
"description": "Toggles the status of the conversation between open and resolved",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"userApiKey": [
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agentBotApiKey": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
|
@ -428,7 +479,8 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"open",
|
"open",
|
||||||
"resolved"
|
"resolved",
|
||||||
|
"bot"
|
||||||
],
|
],
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "The status of the conversation"
|
"description": "The status of the conversation"
|
||||||
|
@ -500,6 +552,18 @@
|
||||||
"operationId": "conversationNewMessage",
|
"operationId": "conversationNewMessage",
|
||||||
"summary": "Create New Message",
|
"summary": "Create New Message",
|
||||||
"description": "All the agent replies are created as new messages through this endpoint",
|
"description": "All the agent replies are created as new messages through this endpoint",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"userApiKey": [
|
||||||
|
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agentBotApiKey": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "id",
|
"name": "id",
|
||||||
|
@ -1035,6 +1099,14 @@
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "ID of the inbox"
|
"description": "ID of the inbox"
|
||||||
},
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the inbox"
|
||||||
|
},
|
||||||
|
"website_url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Website URL"
|
||||||
|
},
|
||||||
"channel_type": {
|
"channel_type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The type of the inbox"
|
"description": "The type of the inbox"
|
||||||
|
|
Loading…
Reference in a new issue