Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
949a728977 | ||
|
|
2437da5339 | ||
|
|
2abb7759f2 | ||
|
|
ca1069241f | ||
|
|
a744b1e247 | ||
|
|
559a052e42 | ||
|
|
f855429d9a | ||
|
|
97b30127a6 | ||
|
|
3a7c418139 | ||
|
|
f2e3e5a83f | ||
|
|
23fac0e7b1 | ||
|
|
02ff710709 | ||
|
|
3f9864c74f | ||
|
|
0b9b137723 | ||
|
|
42e392eca4 | ||
|
|
e668f9da20 | ||
|
|
65a78c7c20 | ||
|
|
86d0955794 | ||
|
|
d720e013d4 | ||
|
|
2f52af6e2d | ||
|
|
d59415ff5d | ||
|
|
14e4ec81c6 | ||
|
|
cbd24dbdb2 |
@@ -3,6 +3,8 @@ class Campaigns::CampaignConversationBuilder
|
|||||||
|
|
||||||
def perform
|
def perform
|
||||||
@contact_inbox = ContactInbox.find(@contact_inbox_id)
|
@contact_inbox = ContactInbox.find(@contact_inbox_id)
|
||||||
|
return unless @contact_inbox.inbox.active?
|
||||||
|
|
||||||
@campaign = @contact_inbox.inbox.campaigns.find_by!(display_id: campaign_display_id)
|
@campaign = @contact_inbox.inbox.campaigns.find_by!(display_id: campaign_display_id)
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ class ConversationBuilder
|
|||||||
pattr_initialize [:params!, :contact_inbox!]
|
pattr_initialize [:params!, :contact_inbox!]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
raise CustomExceptions::InboxDisabled unless @contact_inbox.inbox.active?
|
||||||
|
|
||||||
look_up_exising_conversation || create_new_conversation
|
look_up_exising_conversation || create_new_conversation
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
|||||||
def perform
|
def perform
|
||||||
# This channel might require reauthorization, may be owner might have changed the fb password
|
# This channel might require reauthorization, may be owner might have changed the fb password
|
||||||
return if @inbox.channel.reauthorization_required?
|
return if @inbox.channel.reauthorization_required?
|
||||||
|
return unless @inbox.active?
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
build_contact_inbox
|
build_contact_inbox
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil
|
|||||||
|
|
||||||
def perform
|
def perform
|
||||||
return if @inbox.channel.reauthorization_required?
|
return if @inbox.channel.reauthorization_required?
|
||||||
|
return unless @inbox.active?
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
build_message
|
build_message
|
||||||
|
|||||||
@@ -22,14 +22,15 @@ class Messages::MessageBuilder
|
|||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
raise CustomExceptions::InboxDisabled unless @conversation.inbox.active?
|
||||||
|
|
||||||
@message = @conversation.messages.build(message_params)
|
@message = @conversation.messages.build(message_params)
|
||||||
process_attachments
|
process_attachments
|
||||||
process_emails
|
process_emails
|
||||||
# When the message has no quoted content, it will just be rendered as a regular message
|
# When the message has no quoted content, it will just be rendered as a regular message
|
||||||
# The frontend is equipped to handle this case
|
# The frontend is equipped to handle this case
|
||||||
process_email_content
|
process_email_content
|
||||||
@message.save!
|
@message.tap(&:save!)
|
||||||
@message
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::Conversations::BaseController
|
class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::Conversations::BaseController
|
||||||
before_action :ensure_api_inbox, only: :update
|
before_action :ensure_api_inbox, only: :update
|
||||||
|
before_action :ensure_inbox_active, only: [:create, :retry]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@messages = message_finder.perform
|
@messages = message_finder.perform
|
||||||
@@ -9,6 +10,8 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
|||||||
user = Current.user || @resource
|
user = Current.user || @resource
|
||||||
mb = Messages::MessageBuilder.new(user, @conversation, params)
|
mb = Messages::MessageBuilder.new(user, @conversation, params)
|
||||||
@message = mb.perform
|
@message = mb.perform
|
||||||
|
rescue CustomExceptions::InboxDisabled
|
||||||
|
render_inbox_disabled_error
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
render_could_not_create_error(e.message)
|
render_could_not_create_error(e.message)
|
||||||
end
|
end
|
||||||
@@ -32,6 +35,8 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
|||||||
service.perform
|
service.perform
|
||||||
message.update!(content_attributes: {})
|
message.update!(content_attributes: {})
|
||||||
::SendReplyJob.perform_later(message.id)
|
::SendReplyJob.perform_later(message.id)
|
||||||
|
rescue CustomExceptions::InboxDisabled
|
||||||
|
render_inbox_disabled_error
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
render_could_not_create_error(e.message)
|
render_could_not_create_error(e.message)
|
||||||
end
|
end
|
||||||
@@ -80,4 +85,8 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
|||||||
# Only API inboxes can update messages
|
# Only API inboxes can update messages
|
||||||
render json: { error: 'Message status update is only allowed for API inboxes' }, status: :forbidden unless @conversation.inbox.api?
|
render json: { error: 'Message status update is only allowed for API inboxes' }, status: :forbidden unless @conversation.inbox.api?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_inbox_active
|
||||||
|
render_inbox_disabled_error unless @conversation.inbox.active?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
|
|
||||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||||
authorize @inbox, :show?
|
authorize @inbox, :show?
|
||||||
|
render_inbox_disabled_error unless @inbox.active?
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact
|
def contact
|
||||||
@@ -214,6 +215,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
# and deprecate the support of passing only source_id as the param
|
# and deprecate the support of passing only source_id as the param
|
||||||
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
||||||
authorize @contact_inbox.inbox, :show?
|
authorize @contact_inbox.inbox, :show?
|
||||||
|
render_inbox_disabled_error unless @contact_inbox.inbox.active?
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
render json: { error: 'source_id should be unique' }, status: :unprocessable_entity
|
render json: { error: 'source_id should be unique' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -221,12 +223,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
return if @inbox.blank? || @contact.blank?
|
return if @inbox.blank? || @contact.blank?
|
||||||
|
|
||||||
ContactInboxBuilder.new(
|
ContactInboxBuilder.new(contact: @contact, inbox: @inbox, source_id: params[:source_id], hmac_verified: hmac_verified?).perform
|
||||||
contact: @contact,
|
|
||||||
inbox: @inbox,
|
|
||||||
source_id: params[:source_id],
|
|
||||||
hmac_verified: hmac_verified?
|
|
||||||
).perform
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_finder
|
def conversation_finder
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||||||
def normalized_branded_email_layout = params[:branded_email_layout] == 'null' ? nil : params[:branded_email_layout]
|
def normalized_branded_email_layout = params[:branded_email_layout] == 'null' ? nil : params[:branded_email_layout]
|
||||||
|
|
||||||
def inbox_attributes
|
def inbox_attributes
|
||||||
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
[:name, :avatar, :active, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
||||||
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name,
|
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name,
|
||||||
{ csat_config: [:display_type, :message, :button_text, :language,
|
{ csat_config: [:display_type, :message, :button_text, :language,
|
||||||
|
|||||||
@@ -90,4 +90,8 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||||||
message_type: :incoming
|
message_type: :incoming
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_inbox_active
|
||||||
|
render_inbox_disabled_error unless @web_widget.inbox.active?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
|
class Api::V1::Widget::ConfigsController < Api::V1::Widget::BaseController
|
||||||
|
before_action :ensure_inbox_active
|
||||||
before_action :set_global_config
|
before_action :set_global_config
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||||
include WidgetHelper
|
include WidgetHelper
|
||||||
|
|
||||||
|
before_action :ensure_inbox_active, only: [:update, :set_user, :destroy_custom_attributes]
|
||||||
before_action :validate_hmac, only: [:set_user]
|
before_action :validate_hmac, only: [:set_user]
|
||||||
before_action :validate_hmac_for_identified_update, only: [:update]
|
before_action :validate_hmac_for_identified_update, only: [:update]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
include Events::Types
|
include Events::Types
|
||||||
|
|
||||||
|
DISABLED_INBOX_ACTIONS = [
|
||||||
|
:create, :toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes, :transcript
|
||||||
|
].freeze
|
||||||
|
|
||||||
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
|
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
|
||||||
|
before_action :ensure_inbox_active, only: DISABLED_INBOX_ACTIONS
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsController
|
class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsController
|
||||||
include WebsiteTokenHelper
|
include WebsiteTokenHelper
|
||||||
before_action :set_web_widget
|
before_action :set_web_widget
|
||||||
|
before_action :ensure_inbox_active
|
||||||
before_action :set_contact
|
before_action :set_contact
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -8,4 +9,15 @@ class Api::V1::Widget::DirectUploadsController < ActiveStorage::DirectUploadsCon
|
|||||||
|
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_inbox_active
|
||||||
|
return if @web_widget.inbox.active?
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
error: 'inbox_disabled',
|
||||||
|
message: 'This inbox is currently disabled'
|
||||||
|
}, status: :forbidden
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
|
class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
|
||||||
include Events::Types
|
include Events::Types
|
||||||
|
|
||||||
|
before_action :ensure_inbox_active
|
||||||
|
|
||||||
def create
|
def create
|
||||||
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox,
|
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox,
|
||||||
event_info: permitted_params[:event_info].to_h.merge(event_info))
|
event_info: permitted_params[:event_info].to_h.merge(event_info))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class Api::V1::Widget::Integrations::DyteController < Api::V1::Widget::BaseController
|
class Api::V1::Widget::Integrations::DyteController < Api::V1::Widget::BaseController
|
||||||
|
before_action :ensure_inbox_active
|
||||||
before_action :set_message
|
before_action :set_message
|
||||||
|
|
||||||
def add_participant_to_meeting
|
def add_participant_to_meeting
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class Api::V1::Widget::LabelsController < Api::V1::Widget::BaseController
|
class Api::V1::Widget::LabelsController < Api::V1::Widget::BaseController
|
||||||
|
before_action :ensure_inbox_active
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if conversation.present? && label_defined_in_account?
|
if conversation.present? && label_defined_in_account?
|
||||||
conversation.label_list.add(permitted_params[:label])
|
conversation.label_list.add(permitted_params[:label])
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||||
|
before_action :ensure_inbox_active, only: [:create, :update]
|
||||||
before_action :set_conversation, only: [:create]
|
before_action :set_conversation, only: [:create]
|
||||||
before_action :set_message, only: [:update]
|
before_action :set_message, only: [:update]
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ module RequestExceptionHandler
|
|||||||
|
|
||||||
included do
|
included do
|
||||||
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid
|
||||||
|
rescue_from CustomExceptions::InboxDisabled, with: :render_inbox_disabled_error
|
||||||
rescue_from CustomExceptions::Inbox::LimitExceeded, with: :render_error_response
|
rescue_from CustomExceptions::Inbox::LimitExceeded, with: :render_error_response
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -45,6 +46,13 @@ module RequestExceptionHandler
|
|||||||
render json: { error: sanitized_error_message(error) }, status: :unprocessable_entity
|
render json: { error: sanitized_error_message(error) }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_inbox_disabled_error(_exception = nil)
|
||||||
|
render json: {
|
||||||
|
error: 'inbox_disabled',
|
||||||
|
message: 'This inbox is currently disabled'
|
||||||
|
}, status: :forbidden
|
||||||
|
end
|
||||||
|
|
||||||
def render_payment_required(message)
|
def render_payment_required(message)
|
||||||
render json: { error: message }, status: :payment_required
|
render json: { error: message }, status: :payment_required
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
class Platform::Api::V1::InboxesController < PlatformController
|
||||||
|
before_action :set_account
|
||||||
|
before_action :validate_account_permissible
|
||||||
|
before_action :set_inbox
|
||||||
|
|
||||||
|
def disable
|
||||||
|
update_active_state(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable
|
||||||
|
update_active_state(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:account_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_account_permissible
|
||||||
|
return if @platform_app.platform_app_permissibles.find_by(permissible: @account)
|
||||||
|
|
||||||
|
render json: { error: 'Non permissible resource' }, status: :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_inbox
|
||||||
|
@inbox = @account.inboxes.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_active_state(active)
|
||||||
|
@inbox.update!(active: active)
|
||||||
|
render json: { success: true, inbox_id: @inbox.id, active: @inbox.active }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesController
|
class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesController
|
||||||
before_action :contact_inbox, except: [:create]
|
before_action :contact_inbox, except: [:create]
|
||||||
|
before_action :ensure_inbox_active, only: [:create, :update]
|
||||||
before_action :process_hmac
|
before_action :process_hmac
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
@@ -50,4 +51,8 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
|||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_inbox_active
|
||||||
|
render_inbox_disabled_error unless @inbox_channel.inbox.active?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
|
class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController
|
||||||
include Events::Types
|
include Events::Types
|
||||||
before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status]
|
before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status]
|
||||||
|
before_action :ensure_inbox_active, only: [:create, :toggle_typing, :toggle_status]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@conversations = @contact_inbox.hmac_verified? ? @contact_inbox.contact.conversations : @contact_inbox.conversations
|
@conversations = @contact_inbox.hmac_verified? ? @contact_inbox.contact.conversations : @contact_inbox.conversations
|
||||||
@@ -64,4 +65,8 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
|||||||
def conversation_params
|
def conversation_params
|
||||||
params.permit(custom_attributes: {})
|
params.permit(custom_attributes: {})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_inbox_active
|
||||||
|
render_inbox_disabled_error unless @contact_inbox.inbox.active?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesController
|
class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesController
|
||||||
before_action :set_message, only: [:update]
|
before_action :set_message, only: [:update]
|
||||||
|
before_action :ensure_inbox_active, only: [:create, :update]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@messages = @conversation.nil? ? [] : message_finder.perform
|
@messages = @conversation.nil? ? [] : message_finder.perform
|
||||||
@@ -70,4 +71,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
|
|||||||
def check_csat_locked
|
def check_csat_locked
|
||||||
(Time.zone.now.to_date - @message.created_at.to_date).to_i > 14 and @message.content_type == 'input_csat'
|
(Time.zone.now.to_date - @message.created_at.to_date).to_i > 14 and @message.content_type == 'input_csat'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_inbox_active
|
||||||
|
render_inbox_disabled_error unless @conversation.inbox.active?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ class WidgetsController < ActionController::Base
|
|||||||
before_action :set_global_config
|
before_action :set_global_config
|
||||||
before_action :set_web_widget
|
before_action :set_web_widget
|
||||||
before_action :ensure_account_is_active
|
before_action :ensure_account_is_active
|
||||||
|
before_action :ensure_inbox_active
|
||||||
before_action :ensure_location_is_supported
|
before_action :ensure_location_is_supported
|
||||||
before_action :set_token
|
before_action :set_token
|
||||||
before_action :set_contact
|
before_action :set_contact
|
||||||
@@ -62,6 +63,10 @@ class WidgetsController < ActionController::Base
|
|||||||
render json: { error: 'Account is suspended' }, status: :unauthorized unless @web_widget.inbox.account.active?
|
render json: { error: 'Account is suspended' }, status: :unauthorized unless @web_widget.inbox.account.active?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_inbox_active
|
||||||
|
head :not_found unless @web_widget.inbox.active?
|
||||||
|
end
|
||||||
|
|
||||||
def ensure_location_is_supported; end
|
def ensure_location_is_supported; end
|
||||||
|
|
||||||
def additional_attributes
|
def additional_attributes
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
@@ -7,13 +8,21 @@ defineProps({
|
|||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :title="inbox.name" class="flex items-center gap-0.5 min-w-0">
|
<div :title="inbox.name" class="flex items-center gap-1 min-w-0">
|
||||||
<ChannelIcon :inbox="inbox" class="size-4 flex-shrink-0 text-n-slate-11" />
|
<ChannelIcon :inbox="inbox" class="size-4 flex-shrink-0 text-n-slate-11" />
|
||||||
<span class="truncate text-body-main text-n-slate-11">
|
<span class="truncate text-body-main text-n-slate-11">
|
||||||
{{ inbox.name }}
|
{{ inbox.name }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="inbox.active === false"
|
||||||
|
class="inline-flex h-4 shrink-0 items-center rounded-md bg-n-alpha-2 px-1 text-label-mini font-medium text-n-slate-11"
|
||||||
|
>
|
||||||
|
{{ t('INBOX_MGMT.DISABLED') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
@@ -7,13 +8,21 @@ defineProps({
|
|||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :title="inbox.name" class="flex items-center gap-0.5 min-w-0">
|
<div :title="inbox.name" class="flex items-center gap-1 min-w-0">
|
||||||
<ChannelIcon :inbox="inbox" class="size-4 flex-shrink-0 text-n-slate-11" />
|
<ChannelIcon :inbox="inbox" class="size-4 flex-shrink-0 text-n-slate-11" />
|
||||||
<span class="truncate text-label-small text-n-slate-11">
|
<span class="truncate text-label-small text-n-slate-11">
|
||||||
{{ inbox.name }}
|
{{ inbox.name }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="inbox.active === false"
|
||||||
|
class="inline-flex h-4 shrink-0 items-center rounded-md bg-n-alpha-2 px-1 text-label-mini font-medium text-n-slate-11"
|
||||||
|
>
|
||||||
|
{{ t('INBOX_MGMT.DISABLED') }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"DESCRIPTION": "A channel is the mode of communication your customer chooses to interact with you. An inbox is where you manage interactions for a specific channel. It can include communications from various sources such as email, live chat, and social media.",
|
"DESCRIPTION": "A channel is the mode of communication your customer chooses to interact with you. An inbox is where you manage interactions for a specific channel. It can include communications from various sources such as email, live chat, and social media.",
|
||||||
"LEARN_MORE": "Learn more about inboxes",
|
"LEARN_MORE": "Learn more about inboxes",
|
||||||
"COUNT": "{n} inbox | {n} inboxes",
|
"COUNT": "{n} inbox | {n} inboxes",
|
||||||
|
"DISABLED": "Disabled",
|
||||||
"SEARCH_PLACEHOLDER": "Search inboxes...",
|
"SEARCH_PLACEHOLDER": "Search inboxes...",
|
||||||
"NO_RESULTS": "No inboxes found matching your search",
|
"NO_RESULTS": "No inboxes found matching your search",
|
||||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||||
|
|||||||
@@ -142,10 +142,20 @@ const openDelete = inbox => {
|
|||||||
>
|
>
|
||||||
<ChannelIcon class="size-6 text-n-slate-10" :inbox="inbox" />
|
<ChannelIcon class="size-6 text-n-slate-10" :inbox="inbox" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-start gap-1">
|
<div class="flex flex-col items-start gap-1 min-w-0">
|
||||||
<span class="block text-heading-3 text-n-slate-12 capitalize">
|
<div class="flex items-center gap-1.5 min-w-0">
|
||||||
{{ inbox.name }}
|
<span
|
||||||
</span>
|
class="block text-heading-3 text-n-slate-12 capitalize truncate"
|
||||||
|
>
|
||||||
|
{{ inbox.name }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="inbox.active === false"
|
||||||
|
class="inline-flex h-5 shrink-0 items-center rounded-md bg-n-alpha-2 px-1.5 text-label-mini font-medium text-n-slate-11"
|
||||||
|
>
|
||||||
|
{{ $t('INBOX_MGMT.DISABLED') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<ChannelName
|
<ChannelName
|
||||||
:channel-type="inbox.channel_type"
|
:channel-type="inbox.channel_type"
|
||||||
:medium="inbox.medium"
|
:medium="inbox.medium"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class ConversationReplyEmailJob < ApplicationJob
|
|||||||
def perform(conversation_id, last_queued_id)
|
def perform(conversation_id, last_queued_id)
|
||||||
conversation = Conversation.find(conversation_id)
|
conversation = Conversation.find(conversation_id)
|
||||||
return unless conversation.account.active?
|
return unless conversation.account.active?
|
||||||
|
return unless conversation.inbox.active?
|
||||||
|
|
||||||
if conversation.messages.incoming&.last&.content_type == 'incoming_email'
|
if conversation.messages.incoming&.last&.content_type == 'incoming_email'
|
||||||
ConversationReplyMailer.with(account: conversation.account).reply_without_summary(conversation, last_queued_id).deliver_later
|
ConversationReplyMailer.with(account: conversation.account).reply_without_summary(conversation, last_queued_id).deliver_later
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class Inboxes::FetchImapEmailInboxesJob < ApplicationJob
|
|||||||
|
|
||||||
def should_fetch_emails?(inbox)
|
def should_fetch_emails?(inbox)
|
||||||
return false if inbox.account.suspended?
|
return false if inbox.account.suspended?
|
||||||
|
return false unless inbox.active?
|
||||||
return false unless inbox.channel.imap_enabled
|
return false unless inbox.channel.imap_enabled
|
||||||
return false if inbox.channel.reauthorization_required?
|
return false if inbox.channel.reauthorization_required?
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class Inboxes::FetchImapEmailsJob < MutexApplicationJob
|
|||||||
private
|
private
|
||||||
|
|
||||||
def should_fetch_email?(channel)
|
def should_fetch_email?(channel)
|
||||||
channel.imap_enabled? && !channel.reauthorization_required?
|
channel.inbox.active? && channel.imap_enabled? && !channel.reauthorization_required?
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_email_for_channel(channel, interval)
|
def process_email_for_channel(channel, interval)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
class SendReplyJob < ApplicationJob
|
class SendReplyJob < ApplicationJob
|
||||||
queue_as :high
|
queue_as :high
|
||||||
|
|
||||||
|
INBOX_DISABLED_ERROR = 'This inbox is currently disabled'.freeze
|
||||||
|
|
||||||
CHANNEL_SERVICES = {
|
CHANNEL_SERVICES = {
|
||||||
'Channel::TwitterProfile' => ::Twitter::SendOnTwitterService,
|
'Channel::TwitterProfile' => ::Twitter::SendOnTwitterService,
|
||||||
'Channel::TwilioSms' => ::Twilio::SendOnTwilioService,
|
'Channel::TwilioSms' => ::Twilio::SendOnTwilioService,
|
||||||
@@ -18,6 +20,7 @@ class SendReplyJob < ApplicationJob
|
|||||||
def perform(message_id)
|
def perform(message_id)
|
||||||
message = Message.find(message_id)
|
message = Message.find(message_id)
|
||||||
channel_name = message.conversation.inbox.channel.class.to_s
|
channel_name = message.conversation.inbox.channel.class.to_s
|
||||||
|
return handle_disabled_inbox(message, channel_name) unless message.inbox.active?
|
||||||
|
|
||||||
return send_on_facebook_page(message) if channel_name == 'Channel::FacebookPage'
|
return send_on_facebook_page(message) if channel_name == 'Channel::FacebookPage'
|
||||||
|
|
||||||
@@ -29,6 +32,61 @@ class SendReplyJob < ApplicationJob
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def handle_disabled_inbox(message, channel_name)
|
||||||
|
return unless send_service_available?(channel_name)
|
||||||
|
return unless deliverable_reply?(message, channel_name)
|
||||||
|
|
||||||
|
mark_message_failed(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_service_available?(channel_name)
|
||||||
|
channel_name == 'Channel::FacebookPage' || CHANNEL_SERVICES.key?(channel_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def deliverable_reply?(message, channel_name)
|
||||||
|
return false unless deliverable_message?(message)
|
||||||
|
return message.email_notifiable_message? if email_channel?(channel_name)
|
||||||
|
return email_notification_deliverable?(message, channel_name) if email_notification_channel?(channel_name)
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def deliverable_message?(message)
|
||||||
|
return false if message.private?
|
||||||
|
return false unless message.outgoing? || message.template?
|
||||||
|
return false if message.source_id.present?
|
||||||
|
return false if message.content_type == 'voice_call'
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_channel?(channel_name)
|
||||||
|
channel_name == 'Channel::Email'
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_notification_channel?(channel_name)
|
||||||
|
%w[Channel::WebWidget Channel::Api].include?(channel_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_notification_deliverable?(message, channel_name)
|
||||||
|
return false unless message.email_notifiable_message?
|
||||||
|
return false if message.conversation.contact.email.blank?
|
||||||
|
return false unless message.account.within_email_rate_limit?
|
||||||
|
|
||||||
|
email_notification_enabled?(message.inbox, channel_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_notification_enabled?(inbox, channel_name)
|
||||||
|
return inbox.channel.continuity_via_email if channel_name == 'Channel::WebWidget'
|
||||||
|
return inbox.account.feature_enabled?('email_continuity_on_api_channel') if channel_name == 'Channel::Api'
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_message_failed(message)
|
||||||
|
Messages::StatusUpdateService.new(message, 'failed', INBOX_DISABLED_ERROR).perform
|
||||||
|
end
|
||||||
|
|
||||||
def send_on_facebook_page(message)
|
def send_on_facebook_page(message)
|
||||||
if message.conversation.additional_attributes['type'] == 'instagram_direct_message'
|
if message.conversation.additional_attributes['type'] == 'instagram_direct_message'
|
||||||
::Instagram::Messenger::SendOnInstagramService.new(message: message).perform
|
::Instagram::Messenger::SendOnInstagramService.new(message: message).perform
|
||||||
|
|||||||
@@ -57,9 +57,11 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
|
|||||||
|
|
||||||
next if channel.blank?
|
next if channel.blank?
|
||||||
|
|
||||||
if (event_name = event_name(messaging))
|
event_name = event_name(messaging)
|
||||||
send(event_name, messaging, channel)
|
next if event_name.blank?
|
||||||
end
|
next unless channel.inbox.active? || event_name == :read
|
||||||
|
|
||||||
|
send(event_name, messaging, channel)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
|
|||||||
end
|
end
|
||||||
|
|
||||||
def event_name(messaging)
|
def event_name(messaging)
|
||||||
@event_name ||= SUPPORTED_EVENTS.find { |key| messaging.key?(key) }
|
SUPPORTED_EVENTS.find { |key| messaging.key?(key) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def message(messaging, channel)
|
def message(messaging, channel)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class Webhooks::LineEventsJob < ApplicationJob
|
|||||||
def perform(params: {}, signature: '', post_body: '')
|
def perform(params: {}, signature: '', post_body: '')
|
||||||
@params = params
|
@params = params
|
||||||
return unless valid_event_payload?
|
return unless valid_event_payload?
|
||||||
|
return unless @channel.inbox.active?
|
||||||
return unless valid_post_body?(post_body, signature)
|
return unless valid_post_body?(post_body, signature)
|
||||||
|
|
||||||
Line::IncomingMessageService.new(inbox: @channel.inbox, params: @params['line'].with_indifferent_access).perform
|
Line::IncomingMessageService.new(inbox: @channel.inbox, params: @params['line'].with_indifferent_access).perform
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class Webhooks::SmsEventsJob < ApplicationJob
|
|||||||
|
|
||||||
channel = Channel::Sms.find_by(phone_number: params[:to])
|
channel = Channel::Sms.find_by(phone_number: params[:to])
|
||||||
return unless channel
|
return unless channel
|
||||||
|
return if incoming_event?(params) && !channel.inbox.active?
|
||||||
|
|
||||||
process_event_params(channel, params)
|
process_event_params(channel, params)
|
||||||
end
|
end
|
||||||
@@ -25,4 +26,8 @@ class Webhooks::SmsEventsJob < ApplicationJob
|
|||||||
def delivery_event?(params)
|
def delivery_event?(params)
|
||||||
params[:type] == 'message-delivered' || params[:type] == 'message-failed'
|
params[:type] == 'message-delivered' || params[:type] == 'message-failed'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def incoming_event?(params)
|
||||||
|
params[:type] == 'message-received'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class Webhooks::TelegramEventsJob < ApplicationJob
|
|||||||
|
|
||||||
channel = Channel::Telegram.find_by(bot_token: params[:bot_token])
|
channel = Channel::Telegram.find_by(bot_token: params[:bot_token])
|
||||||
|
|
||||||
if channel_is_inactive?(channel)
|
if channel_is_inactive?(channel, params)
|
||||||
log_inactive_channel(channel, params)
|
log_inactive_channel(channel, params)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -16,13 +16,18 @@ class Webhooks::TelegramEventsJob < ApplicationJob
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def channel_is_inactive?(channel)
|
def channel_is_inactive?(channel, params)
|
||||||
return true if channel.blank?
|
return true if channel.blank?
|
||||||
return true unless channel.account.active?
|
return true unless channel.account.active?
|
||||||
|
return true unless channel.inbox.active? || update_message_event?(params)
|
||||||
|
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_message_event?(params)
|
||||||
|
params.dig(:telegram, :edited_message).present? || params.dig(:telegram, :edited_business_message).present?
|
||||||
|
end
|
||||||
|
|
||||||
def log_inactive_channel(channel, params)
|
def log_inactive_channel(channel, params)
|
||||||
message = if channel&.id
|
message = if channel&.id
|
||||||
"Account #{channel.account.id} is not active for channel #{channel.id}"
|
"Account #{channel.account.id} is not active for channel #{channel.id}"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class Webhooks::TiktokEventsJob < MutexApplicationJob
|
|||||||
def channel_is_inactive?
|
def channel_is_inactive?
|
||||||
return true if channel.blank?
|
return true if channel.blank?
|
||||||
return true unless channel.account.active?
|
return true unless channel.account.active?
|
||||||
|
return true unless channel.inbox.active? || event_name == 'im_mark_read_msg'
|
||||||
|
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class Webhooks::WhatsappEventsJob < MutexApplicationJob
|
|||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
channel = find_channel_from_whatsapp_business_payload(params)
|
channel = find_channel_from_whatsapp_business_payload(params)
|
||||||
|
|
||||||
if channel_is_inactive?(channel)
|
if channel_is_inactive?(channel, params)
|
||||||
Rails.logger.warn("Inactive WhatsApp channel: #{channel&.phone_number || "unknown - #{params[:phone_number]}"}")
|
Rails.logger.warn("Inactive WhatsApp channel: #{channel&.phone_number || "unknown - #{params[:phone_number]}"}")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -124,15 +124,29 @@ class Webhooks::WhatsappEventsJob < MutexApplicationJob
|
|||||||
].compact_blank.first
|
].compact_blank.first
|
||||||
end
|
end
|
||||||
|
|
||||||
def channel_is_inactive?(channel)
|
def channel_is_inactive?(channel, params)
|
||||||
return true if channel.blank?
|
return true if channel.blank?
|
||||||
# Only skip for embedded signup when reauth is required; manual flow uses API keys and should still receive webhooks
|
# Only skip for embedded signup when reauth is required; manual flow uses API keys and should still receive webhooks
|
||||||
return true if channel.reauthorization_required? && embedded_signup_channel?(channel)
|
return true if channel.reauthorization_required? && embedded_signup_channel?(channel)
|
||||||
return true unless channel.account.active?
|
return true unless channel.account.active?
|
||||||
|
return true unless channel.inbox.active? || existing_message_update_event?(params)
|
||||||
|
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status_update_event?(params)
|
||||||
|
value = params.dig(:entry, 0, :changes, 0, :value) || params
|
||||||
|
value[:statuses].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def existing_message_update_event?(params)
|
||||||
|
status_update_event?(params) || call_event?(params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def call_event?(params)
|
||||||
|
params.dig(:entry, 0, :changes, 0, :field) == 'calls'
|
||||||
|
end
|
||||||
|
|
||||||
def embedded_signup_channel?(channel)
|
def embedded_signup_channel?(channel)
|
||||||
(channel.provider_config || {}).to_h['source'] == 'embedded_signup'
|
(channel.provider_config || {}).to_h['source'] == 'embedded_signup'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ class AgentBotListener < BaseListener
|
|||||||
private
|
private
|
||||||
|
|
||||||
def agent_bots_for(inbox, conversation = nil)
|
def agent_bots_for(inbox, conversation = nil)
|
||||||
|
return [] unless inbox.active?
|
||||||
|
|
||||||
bots = []
|
bots = []
|
||||||
bots << conversation.assignee_agent_bot if conversation&.assignee_agent_bot.present?
|
bots << conversation.assignee_agent_bot if conversation&.assignee_agent_bot.present?
|
||||||
inbox_bot = active_inbox_agent_bot(inbox)
|
inbox_bot = active_inbox_agent_bot(inbox)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class AutomationRuleListener < BaseListener
|
|||||||
def message_created(event)
|
def message_created(event)
|
||||||
message = event.data[:message]
|
message = event.data[:message]
|
||||||
|
|
||||||
|
return unless message.inbox.active?
|
||||||
return if ignore_message_created_event?(event)
|
return if ignore_message_created_event?(event)
|
||||||
|
|
||||||
account = message.try(:account)
|
account = message.try(:account)
|
||||||
@@ -39,10 +40,11 @@ class AutomationRuleListener < BaseListener
|
|||||||
def process_conversation_event(event, event_name)
|
def process_conversation_event(event, event_name)
|
||||||
return if performed_by_automation?(event)
|
return if performed_by_automation?(event)
|
||||||
|
|
||||||
auto_reply_skip_events = %w[conversation_created conversation_opened]
|
return if auto_reply_event?(event, event_name)
|
||||||
return if auto_reply_skip_events.include?(event_name) && ignore_auto_reply_event?(event)
|
|
||||||
|
|
||||||
conversation = event.data[:conversation]
|
conversation = event.data[:conversation]
|
||||||
|
return unless conversation.inbox.active?
|
||||||
|
|
||||||
account = conversation.account
|
account = conversation.account
|
||||||
changed_attributes = event.data[:changed_attributes]
|
changed_attributes = event.data[:changed_attributes]
|
||||||
|
|
||||||
@@ -74,6 +76,10 @@ class AutomationRuleListener < BaseListener
|
|||||||
event.data[:performed_by].present? && event.data[:performed_by].instance_of?(AutomationRule)
|
event.data[:performed_by].present? && event.data[:performed_by].instance_of?(AutomationRule)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_reply_event?(event, event_name)
|
||||||
|
%w[conversation_created conversation_opened].include?(event_name) && ignore_auto_reply_event?(event)
|
||||||
|
end
|
||||||
|
|
||||||
def ignore_auto_reply_event?(event)
|
def ignore_auto_reply_event?(event)
|
||||||
conversation = event.data[:conversation]
|
conversation = event.data[:conversation]
|
||||||
conversation.additional_attributes['auto_reply'].present?
|
conversation.additional_attributes['auto_reply'].present?
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ class CsatSurveyListener < BaseListener
|
|||||||
conversation = extract_conversation_and_account(event)[0]
|
conversation = extract_conversation_and_account(event)[0]
|
||||||
|
|
||||||
return unless conversation.resolved?
|
return unless conversation.resolved?
|
||||||
|
return unless conversation.inbox.active?
|
||||||
|
|
||||||
CsatSurveyService.new(conversation: conversation).perform
|
CsatSurveyService.new(conversation: conversation).perform
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class ReplyMailbox < ApplicationMailbox
|
|||||||
def process
|
def process
|
||||||
# Return early if no conversation was found (e.g., notification emails, suspended accounts)
|
# Return early if no conversation was found (e.g., notification emails, suspended accounts)
|
||||||
return unless @conversation
|
return unless @conversation
|
||||||
|
return unless @conversation.inbox.active?
|
||||||
|
|
||||||
# Wrap everything in a transaction to ensure atomicity
|
# Wrap everything in a transaction to ensure atomicity
|
||||||
# This prevents orphan conversations if message/attachment creation fails
|
# This prevents orphan conversations if message/attachment creation fails
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class Campaign < ApplicationRecord
|
|||||||
def trigger!
|
def trigger!
|
||||||
return unless one_off?
|
return unless one_off?
|
||||||
return unless feature_enabled?
|
return unless feature_enabled?
|
||||||
|
return unless inbox.active?
|
||||||
return unless mark_processing!
|
return unless mark_processing!
|
||||||
|
|
||||||
execute_campaign
|
execute_campaign
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ class Line::IncomingMessageService
|
|||||||
LINE_STICKER_IMAGE_URL = 'https://stickershop.line-scdn.net/stickershop/v1/sticker/%s/android/sticker.png'.freeze
|
LINE_STICKER_IMAGE_URL = 'https://stickershop.line-scdn.net/stickershop/v1/sticker/%s/android/sticker.png'.freeze
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
return unless @inbox.active?
|
||||||
|
|
||||||
# probably test events
|
# probably test events
|
||||||
return if params[:events].blank?
|
return if params[:events].blank?
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class Mailbox::ConversationFinderStrategies::NewConversationStrategy < Mailbox::
|
|||||||
# The actual persistence happens in ReplyMailbox within a transaction that includes message creation.
|
# The actual persistence happens in ReplyMailbox within a transaction that includes message creation.
|
||||||
def find
|
def find
|
||||||
return nil unless @channel # No valid channel found
|
return nil unless @channel # No valid channel found
|
||||||
|
return nil unless @inbox.active?
|
||||||
return nil unless incoming_email_from_valid_email? # Skip edge cases
|
return nil unless incoming_email_from_valid_email? # Skip edge cases
|
||||||
|
|
||||||
# Check if conversation already exists by in_reply_to
|
# Check if conversation already exists by in_reply_to
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ class Sms::IncomingMessageService
|
|||||||
pattr_initialize [:inbox!, :params!]
|
pattr_initialize [:inbox!, :params!]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
return unless @inbox.active?
|
||||||
|
|
||||||
set_contact
|
set_contact
|
||||||
set_conversation
|
set_conversation
|
||||||
@message = @conversation.messages.create!(
|
@message = @conversation.messages.create!(
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ class Telegram::IncomingMessageService
|
|||||||
pattr_initialize [:inbox!, :params!]
|
pattr_initialize [:inbox!, :params!]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
return unless @inbox.active?
|
||||||
|
|
||||||
# chatwoot doesn't support group conversations at the moment
|
# chatwoot doesn't support group conversations at the moment
|
||||||
transform_business_message!
|
transform_business_message!
|
||||||
return unless private_message?
|
return unless private_message?
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ class Tiktok::MessageService
|
|||||||
pattr_initialize [:channel!, :content!, :outgoing_echo]
|
pattr_initialize [:channel!, :content!, :outgoing_echo]
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
|
return unless channel.inbox.active?
|
||||||
|
|
||||||
if outgoing_message?
|
if outgoing_message?
|
||||||
message = find_message(tt_conversation_id, tt_message_id)
|
message = find_message(tt_conversation_id, tt_message_id)
|
||||||
return if message.present?
|
return if message.present?
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class Twilio::IncomingMessageService
|
|||||||
|
|
||||||
def perform
|
def perform
|
||||||
return if twilio_channel.blank?
|
return if twilio_channel.blank?
|
||||||
|
return unless inbox.active?
|
||||||
|
|
||||||
set_contact
|
set_contact
|
||||||
set_conversation
|
set_conversation
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ class Twitter::DirectMessageParserService < Twitter::WebhooksBaseService
|
|||||||
return if source_app_id == parent_app_id
|
return if source_app_id == parent_app_id
|
||||||
|
|
||||||
set_inbox
|
set_inbox
|
||||||
|
return unless @inbox.active?
|
||||||
|
|
||||||
ensure_contacts
|
ensure_contacts
|
||||||
set_conversation
|
set_conversation
|
||||||
@message = @conversation.messages.create!(
|
@message = @conversation.messages.create!(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class Twitter::TweetParserService < Twitter::WebhooksBaseService
|
|||||||
def perform
|
def perform
|
||||||
set_inbox
|
set_inbox
|
||||||
|
|
||||||
|
return unless @inbox.active?
|
||||||
return if !tweets_enabled? || message_already_exist? || user_has_blocked?
|
return if !tweets_enabled? || message_already_exist? || user_has_blocked?
|
||||||
|
|
||||||
create_message
|
create_message
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class Whatsapp::IncomingMessageBaseService
|
|||||||
if processed_params.try(:[], :statuses).present?
|
if processed_params.try(:[], :statuses).present?
|
||||||
process_statuses
|
process_statuses
|
||||||
elsif messages_data.present?
|
elsif messages_data.present?
|
||||||
|
return unless @inbox.active?
|
||||||
|
|
||||||
process_messages
|
process_messages
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ json.id resource.id
|
|||||||
json.avatar_url resource.try(:avatar_url)
|
json.avatar_url resource.try(:avatar_url)
|
||||||
json.channel_id resource.channel_id
|
json.channel_id resource.channel_id
|
||||||
json.name resource.name
|
json.name resource.name
|
||||||
|
json.active resource.active
|
||||||
json.channel_type resource.channel_type
|
json.channel_type resource.channel_type
|
||||||
json.greeting_enabled resource.greeting_enabled
|
json.greeting_enabled resource.greeting_enabled
|
||||||
json.greeting_message resource.greeting_message
|
json.greeting_message resource.greeting_message
|
||||||
|
|||||||
@@ -581,6 +581,12 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
resources :email_channel_migrations, only: [:create]
|
resources :email_channel_migrations, only: [:create]
|
||||||
|
resources :inboxes, only: [] do
|
||||||
|
member do
|
||||||
|
post :disable
|
||||||
|
post :enable
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddActiveToInboxes < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
add_column :inboxes, :active, :boolean, default: true, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1048,6 +1048,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_07_13_184351) do
|
|||||||
t.boolean "enable_email_collect", default: true
|
t.boolean "enable_email_collect", default: true
|
||||||
t.boolean "csat_survey_enabled", default: false
|
t.boolean "csat_survey_enabled", default: false
|
||||||
t.boolean "allow_messages_after_resolved", default: true
|
t.boolean "allow_messages_after_resolved", default: true
|
||||||
|
t.boolean "active", default: true, null: false
|
||||||
t.jsonb "auto_assignment_config", default: {}
|
t.jsonb "auto_assignment_config", default: {}
|
||||||
t.boolean "lock_to_single_conversation", default: false, null: false
|
t.boolean "lock_to_single_conversation", default: false, null: false
|
||||||
t.bigint "portal_id"
|
t.bigint "portal_id"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseController
|
||||||
before_action :set_call, only: %i[show accept reject terminate upload_recording]
|
before_action :set_call, only: %i[show accept reject terminate upload_recording]
|
||||||
before_action :set_call_context, only: :initiate
|
before_action :set_call_context, only: :initiate
|
||||||
|
before_action :ensure_inbox_active, only: :initiate
|
||||||
before_action :ensure_calling_enabled, only: :initiate
|
before_action :ensure_calling_enabled, only: :initiate
|
||||||
before_action :ensure_sdp_offer, only: :initiate
|
before_action :ensure_sdp_offer, only: :initiate
|
||||||
before_action :ensure_contact_phone, only: :initiate
|
before_action :ensure_contact_phone, only: :initiate
|
||||||
@@ -96,6 +97,10 @@ class Api::V1::Accounts::WhatsappCallsController < Api::V1::Accounts::BaseContro
|
|||||||
render_could_not_create_error(I18n.t('errors.whatsapp.calls.not_enabled'))
|
render_could_not_create_error(I18n.t('errors.whatsapp.calls.not_enabled'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_inbox_active
|
||||||
|
render_inbox_disabled_error unless @inbox.active?
|
||||||
|
end
|
||||||
|
|
||||||
def ensure_sdp_offer
|
def ensure_sdp_offer
|
||||||
return if params[:sdp_offer].present?
|
return if params[:sdp_offer].present?
|
||||||
|
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ class Twilio::VoiceController < ApplicationController
|
|||||||
from_number.start_with?('client:')
|
from_number.start_with?('client:')
|
||||||
end
|
end
|
||||||
|
|
||||||
# A fresh contact-initiated leg on an inbox with inbound calls turned off.
|
# A fresh contact-initiated leg on an inbox that cannot receive calls.
|
||||||
# Reject it so no conference, conversation, or Call row is created.
|
# Reject it so no conference, conversation, or Call row is created.
|
||||||
def reject_inbound?
|
def reject_inbound?
|
||||||
twilio_direction == 'inbound' && !agent_leg?(twilio_from) && !inbox.channel.inbound_calls_enabled?
|
twilio_direction == 'inbound' && !agent_leg?(twilio_from) && (!inbox.active? || !inbox.channel.inbound_calls_enabled?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reject_twiml
|
def reject_twiml
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class Voice::OutboundCallBuilder
|
|||||||
def perform!
|
def perform!
|
||||||
raise ArgumentError, 'Contact phone number required' if contact.phone_number.blank?
|
raise ArgumentError, 'Contact phone number required' if contact.phone_number.blank?
|
||||||
raise ArgumentError, 'Agent required' if user.blank?
|
raise ArgumentError, 'Agent required' if user.blank?
|
||||||
|
raise CustomExceptions::InboxDisabled unless inbox.active?
|
||||||
|
|
||||||
# Claim for the caller if a reused conversation is unassigned at trigger time; wins over auto-assignment.
|
# Claim for the caller if a reused conversation is unassigned at trigger time; wins over auto-assignment.
|
||||||
# New conversations set the assignee at creation instead (see create_conversation!).
|
# New conversations set the assignee at creation instead (see create_conversation!).
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class Whatsapp::IncomingCallService
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_inbound_call(payload)
|
def create_inbound_call(payload)
|
||||||
unless inbox.channel.inbound_calls_enabled?
|
unless inbox.active? && inbox.channel.inbound_calls_enabled?
|
||||||
Rails.logger.info "[WHATSAPP CALL] Inbound calls disabled for inbox #{inbox.id}; rejecting call #{payload[:id]}"
|
Rails.logger.info "[WHATSAPP CALL] Inbound calls disabled for inbox #{inbox.id}; rejecting call #{payload[:id]}"
|
||||||
inbox.channel.provider_service.reject_call(payload[:id])
|
inbox.channel.provider_service.reject_call(payload[:id])
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CustomExceptions::InboxDisabled < CustomExceptions::Base
|
||||||
|
def initialize(data = {})
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def message
|
||||||
|
'This inbox is currently disabled'
|
||||||
|
end
|
||||||
|
|
||||||
|
def error_code
|
||||||
|
'inbox_disabled'
|
||||||
|
end
|
||||||
|
|
||||||
|
def http_status
|
||||||
|
:forbidden
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -31,6 +31,18 @@ RSpec.describe '/api/v1/widget/config', type: :request do
|
|||||||
response_data = response.parsed_body
|
response_data = response.parsed_body
|
||||||
expect(response_data.keys).to include(*response_keys)
|
expect(response_data.keys).to include(*response_keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not initialize config or create a contact when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
post '/api/v1/widget/config',
|
||||||
|
params: params,
|
||||||
|
as: :json
|
||||||
|
end.not_to change(Contact, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with correct website token and valid X-Auth-Token' do
|
context 'with correct website token and valid X-Auth-Token' do
|
||||||
@@ -48,6 +60,17 @@ RSpec.describe '/api/v1/widget/config', type: :request do
|
|||||||
expect(response_data['contact']['pubsub_token']).to eq(contact_inbox.pubsub_token)
|
expect(response_data['contact']['pubsub_token']).to eq(contact_inbox.pubsub_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not initialize config when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
post '/api/v1/widget/config',
|
||||||
|
params: params,
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns 401 if account is suspended' do
|
it 'returns 401 if account is suspended' do
|
||||||
account.update!(status: :suspended)
|
account.update!(status: :suspended)
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
|
|||||||
expect(ContactIdentifyAction).to have_received(:new).with(expected_params)
|
expect(ContactIdentifyAction).to have_received(:new).with(expected_params)
|
||||||
expect(identify_action).to have_received(:perform)
|
expect(identify_action).to have_received(:perform)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not update the contact when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
patch '/api/v1/widget/contact',
|
||||||
|
params: params,
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(ContactIdentifyAction).not_to have_received(:new)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with update contact' do
|
context 'with update contact' do
|
||||||
@@ -255,6 +267,18 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
|
|||||||
|
|
||||||
expect(response).to have_http_status(:unauthorized)
|
expect(response).to have_http_status(:unauthorized)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not set user when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
patch '/api/v1/widget/contact/set_user',
|
||||||
|
params: params.merge(identifier_hash: correct_identifier_hash),
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(ContactIdentifyAction).not_to have_received(:new)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -263,19 +287,32 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
|
|||||||
|
|
||||||
context 'with invalid website token' do
|
context 'with invalid website token' do
|
||||||
it 'returns unauthorized' do
|
it 'returns unauthorized' do
|
||||||
post '/api/v1/widget/destroy_custom_attributes', params: { website_token: '' }
|
post '/api/v1/widget/contact/destroy_custom_attributes', params: { website_token: '' }
|
||||||
expect(response).to have_http_status(:not_found)
|
expect(response).to have_http_status(:not_found)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with correct website token' do
|
context 'with correct website token' do
|
||||||
it 'calls destroy custom attributes' do
|
it 'calls destroy custom attributes' do
|
||||||
post '/api/v1/widget/destroy_custom_attributes',
|
post '/api/v1/widget/contact/destroy_custom_attributes',
|
||||||
params: params,
|
params: params,
|
||||||
headers: { 'X-Auth-Token' => token },
|
headers: { 'X-Auth-Token' => token },
|
||||||
as: :json
|
as: :json
|
||||||
expect(contact.reload.custom_attributes).to eq({})
|
expect(contact.reload.custom_attributes).to eq({})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not destroy custom attributes when the inbox is disabled' do
|
||||||
|
contact.update!(custom_attributes: { 'test' => 'value' })
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
post '/api/v1/widget/contact/destroy_custom_attributes',
|
||||||
|
params: params,
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(contact.reload.custom_attributes).to eq('test' => 'value')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -43,6 +43,18 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
|||||||
expect(json_response['id']).to eq(conversation.display_id)
|
expect(json_response['id']).to eq(conversation.display_id)
|
||||||
expect(json_response['status']).to eq(conversation.status)
|
expect(json_response['status']).to eq(conversation.status)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns the conversation when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
get '/api/v1/widget/conversations',
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
params: { website_token: web_widget.website_token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.parsed_body['id']).to eq(conversation.display_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a conversation but invalid source id' do
|
context 'with a conversation but invalid source id' do
|
||||||
@@ -62,6 +74,19 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /api/v1/widget/conversations' do
|
describe 'POST /api/v1/widget/conversations' do
|
||||||
|
it 'does not create a conversation when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
post '/api/v1/widget/conversations',
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
params: conversation_params,
|
||||||
|
as: :json
|
||||||
|
end.not_to change(Conversation, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
it 'creates a conversation with correct details' do
|
it 'creates a conversation with correct details' do
|
||||||
post '/api/v1/widget/conversations',
|
post '/api/v1/widget/conversations',
|
||||||
headers: { 'X-Auth-Token' => token },
|
headers: { 'X-Auth-Token' => token },
|
||||||
@@ -244,11 +269,40 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
|||||||
|
|
||||||
expect(conversation.reload.contact_last_seen_at).not_to be_nil
|
expect(conversation.reload.contact_last_seen_at).not_to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'updates last seen when the inbox is disabled' do
|
||||||
|
current_time = DateTime.now.utc
|
||||||
|
allow(DateTime).to receive(:now).and_return(current_time)
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
expect(Conversations::UpdateMessageStatusJob).to receive(:perform_later).with(conversation.id, current_time)
|
||||||
|
|
||||||
|
post '/api/v1/widget/conversations/update_last_seen',
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
params: { website_token: web_widget.website_token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(conversation.reload.contact_last_seen_at).not_to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /api/v1/widget/conversations/transcript' do
|
describe 'POST /api/v1/widget/conversations/transcript' do
|
||||||
context 'with a conversation' do
|
context 'with a conversation' do
|
||||||
|
it 'does not send transcript email when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
contact.update!(email: 'test@test.com')
|
||||||
|
expect(ConversationReplyMailer).not_to receive(:with)
|
||||||
|
|
||||||
|
post '/api/v1/widget/conversations/transcript',
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
params: { website_token: web_widget.website_token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
it 'sends transcript email' do
|
it 'sends transcript email' do
|
||||||
contact.update(email: 'test@test.com')
|
contact.update(email: 'test@test.com')
|
||||||
mailer = double
|
mailer = double
|
||||||
|
|||||||
@@ -34,6 +34,24 @@ RSpec.describe '/api/v1/widget/direct_uploads', type: :request do
|
|||||||
json_response = response.parsed_body
|
json_response = response.parsed_body
|
||||||
expect(json_response['content_type']).to eq('image/png')
|
expect(json_response['content_type']).to eq('image/png')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not create direct upload when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
post api_v1_widget_direct_uploads_url,
|
||||||
|
params: {
|
||||||
|
website_token: web_widget.website_token,
|
||||||
|
blob: {
|
||||||
|
filename: 'avatar.png',
|
||||||
|
byte_size: '1234',
|
||||||
|
checksum: 'dsjbsdhbfif3874823mnsdbf',
|
||||||
|
content_type: 'image/png'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headers: { 'X-Auth-Token' => token }
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -34,6 +34,20 @@ RSpec.describe '/api/v1/widget/events', type: :request do
|
|||||||
.with(params[:name], anything, contact_inbox: contact_inbox,
|
.with(params[:name], anything, contact_inbox: contact_inbox,
|
||||||
event_info: { test_id: 'test', browser_language: nil, widget_language: nil, browser: anything })
|
event_info: { test_id: 'test', browser_language: nil, widget_language: nil, browser: anything })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not dispatch events when the inbox is disabled' do
|
||||||
|
token
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
expect(Rails.configuration.dispatcher).not_to receive(:dispatch)
|
||||||
|
|
||||||
|
post '/api/v1/widget/events',
|
||||||
|
params: params,
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -70,6 +70,17 @@ RSpec.describe '/api/v1/widget/integrations/dyte', type: :request do
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not add a participant when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
post add_participant_to_meeting_api_v1_widget_integrations_dyte_url,
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
params: { website_token: web_widget.website_token, message_id: integration_message.id },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ RSpec.describe '/api/v1/widget/labels', type: :request do
|
|||||||
expect(conversation.reload.label_list.count).to eq 1
|
expect(conversation.reload.label_list.count).to eq 1
|
||||||
expect(conversation.reload.label_list.first).to eq 'customer-support'
|
expect(conversation.reload.label_list.first).to eq 'customer-support'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not add labels when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
post '/api/v1/widget/labels',
|
||||||
|
params: params,
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(conversation.reload.label_list.count).to eq 0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with invalid website token' do
|
context 'with invalid website token' do
|
||||||
@@ -67,6 +79,18 @@ RSpec.describe '/api/v1/widget/labels', type: :request do
|
|||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(conversation.reload.label_list.count).to eq 0
|
expect(conversation.reload.label_list.count).to eq 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not remove labels when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
delete "/api/v1/widget/labels/#{params[:label]}",
|
||||||
|
params: params,
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(conversation.reload.label_list.count).to eq 1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with invalid website token' do
|
context 'with invalid website token' do
|
||||||
|
|||||||
@@ -28,6 +28,18 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
|||||||
expect(json_response['meta']).not_to be_empty
|
expect(json_response['meta']).not_to be_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns messages when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
get api_v1_widget_messages_url,
|
||||||
|
params: { website_token: web_widget.website_token },
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(response.parsed_body['payload'].length).to eq(4)
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns empty messages', :skip_before do
|
it 'returns empty messages', :skip_before do
|
||||||
get api_v1_widget_messages_url,
|
get api_v1_widget_messages_url,
|
||||||
params: { website_token: web_widget.website_token },
|
params: { website_token: web_widget.website_token },
|
||||||
@@ -43,6 +55,20 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
|||||||
|
|
||||||
describe 'POST /api/v1/widget/messages' do
|
describe 'POST /api/v1/widget/messages' do
|
||||||
context 'when post request is made' do
|
context 'when post request is made' do
|
||||||
|
it 'does not create message in conversation when the inbox is disabled' do
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
message_params = { content: 'hello world', timestamp: Time.current }
|
||||||
|
|
||||||
|
expect do
|
||||||
|
post api_v1_widget_messages_url,
|
||||||
|
params: { website_token: web_widget.website_token, message: message_params },
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
as: :json
|
||||||
|
end.not_to change(Message, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
it 'creates message in conversation' do
|
it 'creates message in conversation' do
|
||||||
conversation.destroy! # Test all params
|
conversation.destroy! # Test all params
|
||||||
message_params = { content: 'hello world', timestamp: Time.current }
|
message_params = { content: 'hello world', timestamp: Time.current }
|
||||||
@@ -213,6 +239,21 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'PUT /api/v1/widget/messages' do
|
describe 'PUT /api/v1/widget/messages' do
|
||||||
|
context 'when the inbox is disabled' do
|
||||||
|
it 'does not update the message' do
|
||||||
|
message = create(:message, content_type: 'input_email', account: account, inbox: web_widget.inbox, conversation: conversation)
|
||||||
|
web_widget.inbox.update!(active: false)
|
||||||
|
|
||||||
|
put api_v1_widget_message_url(message.id),
|
||||||
|
params: { website_token: web_widget.website_token, contact: { email: Faker::Internet.email } },
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(message.reload.submitted_email).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when put request targets a message from another visitor in the same inbox' do
|
context 'when put request targets a message from another visitor in the same inbox' do
|
||||||
it 'does not update the foreign message' do
|
it 'does not update the foreign message' do
|
||||||
other_contact = create(:contact, account: account, email: nil)
|
other_contact = create(:contact, account: account, email: nil)
|
||||||
|
|||||||
@@ -139,5 +139,18 @@ RSpec.describe 'Public Inbox Contact Conversations API', type: :request do
|
|||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(conversation.reload.contact_last_seen_at).not_to eq contact_last_seen_at
|
expect(conversation.reload.contact_last_seen_at).not_to eq contact_last_seen_at
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'updates the last seen when the inbox is disabled' do
|
||||||
|
current_time = DateTime.now.utc
|
||||||
|
allow(DateTime).to receive(:now).and_return(current_time)
|
||||||
|
api_channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
expect(Conversations::UpdateMessageStatusJob).to receive(:perform_later).with(conversation.id, current_time)
|
||||||
|
|
||||||
|
post update_last_seen_path
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(conversation.reload.contact_last_seen_at).not_to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -208,6 +208,18 @@ RSpec.describe 'WhatsApp Calls API', type: :request do
|
|||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
expect(response.parsed_body['error']).to eq(I18n.t('errors.whatsapp.calls.not_enabled'))
|
expect(response.parsed_body['error']).to eq(I18n.t('errors.whatsapp.calls.not_enabled'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns 403 before initiating the provider call when the inbox is disabled' do
|
||||||
|
inbox.update!(active: false)
|
||||||
|
allow(provider_service).to receive(:initiate_call)
|
||||||
|
|
||||||
|
post "/api/v1/accounts/#{account.id}/whatsapp_calls/initiate",
|
||||||
|
params: { conversation_id: initiate_conversation.display_id, sdp_offer: 'sdp_offer' },
|
||||||
|
headers: agent.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:forbidden)
|
||||||
|
expect(provider_service).not_to have_received(:initiate_call)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /api/v1/accounts/:account_id/whatsapp_calls/:id/upload_recording' do
|
describe 'POST /api/v1/accounts/:account_id/whatsapp_calls/:id/upload_recording' do
|
||||||
|
|||||||
@@ -129,6 +129,23 @@ RSpec.describe 'Twilio::VoiceController', type: :request do
|
|||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(response.body).to include('<Reject')
|
expect(response.body).to include('<Reject')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'rejects the inbound contact leg without building a call when the inbox is disabled' do
|
||||||
|
inbox.update!(active: false)
|
||||||
|
expect(Voice::InboundCallBuilder).not_to receive(:perform!)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
post "/twilio/voice/call/#{digits}", params: {
|
||||||
|
'CallSid' => call_sid,
|
||||||
|
'From' => from_number,
|
||||||
|
'To' => to_number,
|
||||||
|
'Direction' => 'inbound'
|
||||||
|
}
|
||||||
|
end.not_to change(Call, :count)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(response.body).to include('<Reject')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /twilio/voice/status/:phone' do
|
describe 'POST /twilio/voice/status/:phone' do
|
||||||
|
|||||||
@@ -127,5 +127,20 @@ RSpec.describe Voice::OutboundCallBuilder do
|
|||||||
)
|
)
|
||||||
end.to raise_error(ArgumentError, 'Agent required')
|
end.to raise_error(ArgumentError, 'Agent required')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'raises before initiating the provider call when the inbox is disabled' do
|
||||||
|
inbox.update!(active: false)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
described_class.perform!(
|
||||||
|
account: account,
|
||||||
|
inbox: inbox,
|
||||||
|
user: user,
|
||||||
|
contact: contact
|
||||||
|
)
|
||||||
|
end.to raise_error(CustomExceptions::InboxDisabled)
|
||||||
|
|
||||||
|
expect(channel).not_to have_received(:initiate_call)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -44,6 +44,19 @@ describe Whatsapp::IncomingCallService do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when the inbox is disabled' do
|
||||||
|
it 'rejects the call with Meta without creating a Call or Conversation' do
|
||||||
|
inbox.update!(active: false)
|
||||||
|
provider_service = instance_double(Whatsapp::Providers::WhatsappCloudService, reject_call: true)
|
||||||
|
allow(inbox.channel).to receive(:provider_service).and_return(provider_service)
|
||||||
|
|
||||||
|
params = call_payload(event: 'connect', session: { sdp: "v=0\r\n...sdp...", sdp_type: 'offer' })
|
||||||
|
expect { described_class.new(inbox: inbox, params: params).perform }
|
||||||
|
.to not_change(Call, :count).and not_change(Conversation, :count)
|
||||||
|
expect(provider_service).to have_received(:reject_call).with(provider_call_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'inbound connect' do
|
describe 'inbound connect' do
|
||||||
let(:sdp_offer) { "v=0\r\n...sdp..." }
|
let(:sdp_offer) { "v=0\r\n...sdp..." }
|
||||||
let!(:agent) { create(:user, account: account) }
|
let!(:agent) { create(:user, account: account) }
|
||||||
|
|||||||
@@ -30,4 +30,12 @@ RSpec.describe ConversationReplyEmailJob, type: :job do
|
|||||||
described_class.perform_now(conversation.id, 123)
|
described_class.perform_now(conversation.id, 123)
|
||||||
expect(mailer).to have_received(:reply_without_summary)
|
expect(mailer).to have_received(:reply_without_summary)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not send an email when the inbox is disabled before the job runs' do
|
||||||
|
conversation.inbox.update!(active: false)
|
||||||
|
|
||||||
|
described_class.perform_now(conversation.id, 123)
|
||||||
|
|
||||||
|
expect(ConversationReplyMailer).not_to have_received(:with)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -122,5 +122,62 @@ RSpec.describe SendReplyJob do
|
|||||||
message = create(:message, conversation: create(:conversation, inbox: tiktok_channel.inbox))
|
message = create(:message, conversation: create(:conversation, inbox: tiktok_channel.inbox))
|
||||||
expect_mapped_service_to_perform(message, 'Tiktok::SendOnTiktokService')
|
expect_mapped_service_to_perform(message, 'Tiktok::SendOnTiktokService')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'marks the message as failed when the inbox is disabled before the queued send runs' do
|
||||||
|
twilio_channel = create(:channel_twilio_sms)
|
||||||
|
message = create(:message, message_type: :outgoing, conversation: create(:conversation, inbox: twilio_channel.inbox))
|
||||||
|
twilio_channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
expect(Twilio::SendOnTwilioService).not_to receive(:new)
|
||||||
|
|
||||||
|
described_class.perform_now(message.id)
|
||||||
|
|
||||||
|
expect(message.reload).to be_failed
|
||||||
|
expect(message.external_error).to eq('This inbox is currently disabled')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not mark incoming messages as failed when the inbox is disabled' do
|
||||||
|
twilio_channel = create(:channel_twilio_sms)
|
||||||
|
message = create(:message, message_type: :incoming, conversation: create(:conversation, inbox: twilio_channel.inbox))
|
||||||
|
twilio_channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
described_class.perform_now(message.id)
|
||||||
|
|
||||||
|
expect(message.reload).not_to be_failed
|
||||||
|
expect(message.external_error).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not mark private notes as failed when the inbox is disabled' do
|
||||||
|
twilio_channel = create(:channel_twilio_sms)
|
||||||
|
message = create(:message, private: true, conversation: create(:conversation, inbox: twilio_channel.inbox))
|
||||||
|
twilio_channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
described_class.perform_now(message.id)
|
||||||
|
|
||||||
|
expect(message.reload).not_to be_failed
|
||||||
|
expect(message.external_error).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not mark provider echo messages as failed when the inbox is disabled' do
|
||||||
|
twilio_channel = create(:channel_twilio_sms)
|
||||||
|
message = create(:message, source_id: 'provider-message-id', conversation: create(:conversation, inbox: twilio_channel.inbox))
|
||||||
|
twilio_channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
described_class.perform_now(message.id)
|
||||||
|
|
||||||
|
expect(message.reload).not_to be_failed
|
||||||
|
expect(message.external_error).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not mark web widget replies as failed when email continuity is disabled' do
|
||||||
|
webwidget_channel = create(:channel_widget, continuity_via_email: false)
|
||||||
|
message = create(:message, message_type: :outgoing, conversation: create(:conversation, inbox: webwidget_channel.inbox))
|
||||||
|
webwidget_channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
described_class.perform_now(message.id)
|
||||||
|
|
||||||
|
expect(message.reload).not_to be_failed
|
||||||
|
expect(message.external_error).to be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -202,6 +202,24 @@ describe Webhooks::InstagramEventsJob do
|
|||||||
instagram_webhook.perform_now(messaging_seen_event[:entry])
|
instagram_webhook.perform_now(messaging_seen_event[:entry])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'handles messaging_seen callback when the inbox is disabled' do
|
||||||
|
messaging_seen_event = build(:messaging_seen_event).with_indifferent_access
|
||||||
|
instagram_messenger_inbox.update!(active: false)
|
||||||
|
|
||||||
|
expect(Instagram::ReadStatusService).to receive(:new).with(params: messaging_seen_event[:entry][0][:messaging][0],
|
||||||
|
channel: instagram_messenger_inbox.channel).and_call_original
|
||||||
|
instagram_webhook.perform_now(messaging_seen_event[:entry])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create message callbacks when the inbox is disabled' do
|
||||||
|
dm_event = build(:instagram_message_create_event).with_indifferent_access
|
||||||
|
instagram_messenger_inbox.update!(active: false)
|
||||||
|
|
||||||
|
instagram_webhook.perform_now(dm_event[:entry])
|
||||||
|
|
||||||
|
expect(instagram_messenger_inbox.messages.count).to be 0
|
||||||
|
end
|
||||||
|
|
||||||
it 'handles unsupported message' do
|
it 'handles unsupported message' do
|
||||||
unsupported_event = build(:instagram_message_unsupported_event).with_indifferent_access
|
unsupported_event = build(:instagram_message_unsupported_event).with_indifferent_access
|
||||||
sender_id = unsupported_event[:entry][0][:messaging][0][:sender][:id]
|
sender_id = unsupported_event[:entry][0][:messaging][0][:sender][:id]
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ RSpec.describe Webhooks::SmsEventsJob do
|
|||||||
described_class.perform_now(params)
|
described_class.perform_now(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not call Sms::IncomingMessageService for received messages when the inbox is disabled' do
|
||||||
|
sms_channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
expect(Sms::IncomingMessageService).not_to receive(:new)
|
||||||
|
|
||||||
|
described_class.perform_now(params)
|
||||||
|
end
|
||||||
|
|
||||||
it 'calls Sms::DeliveryStatusService if the message type is message-delivered' do
|
it 'calls Sms::DeliveryStatusService if the message type is message-delivered' do
|
||||||
params[:type] = 'message-delivered'
|
params[:type] = 'message-delivered'
|
||||||
process_service = double
|
process_service = double
|
||||||
@@ -64,6 +72,20 @@ RSpec.describe Webhooks::SmsEventsJob do
|
|||||||
described_class.perform_now(params)
|
described_class.perform_now(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'calls Sms::DeliveryStatusService for delivered messages when the inbox is disabled' do
|
||||||
|
sms_channel.inbox.update!(active: false)
|
||||||
|
params[:type] = 'message-delivered'
|
||||||
|
process_service = double
|
||||||
|
allow(Sms::DeliveryStatusService).to receive(:new).and_return(process_service)
|
||||||
|
allow(process_service).to receive(:perform)
|
||||||
|
|
||||||
|
expect(Sms::DeliveryStatusService).to receive(:new).with(channel: sms_channel,
|
||||||
|
params: params[:message].with_indifferent_access)
|
||||||
|
expect(process_service).to receive(:perform)
|
||||||
|
|
||||||
|
described_class.perform_now(params)
|
||||||
|
end
|
||||||
|
|
||||||
it 'calls Sms::DeliveryStatusService if the message type is message-failed' do
|
it 'calls Sms::DeliveryStatusService if the message type is message-failed' do
|
||||||
params[:type] = 'message-failed'
|
params[:type] = 'message-failed'
|
||||||
process_service = double
|
process_service = double
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ RSpec.describe Webhooks::TelegramEventsJob do
|
|||||||
expect(Telegram::IncomingMessageService).not_to receive(:new)
|
expect(Telegram::IncomingMessageService).not_to receive(:new)
|
||||||
described_class.perform_now(params.with_indifferent_access)
|
described_class.perform_now(params.with_indifferent_access)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not process incoming message events when the inbox is disabled' do
|
||||||
|
telegram_channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
expect(Telegram::IncomingMessageService).not_to receive(:new)
|
||||||
|
|
||||||
|
described_class.perform_now(params.with_indifferent_access)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when update message params' do
|
context 'when update message params' do
|
||||||
@@ -60,5 +68,17 @@ RSpec.describe Webhooks::TelegramEventsJob do
|
|||||||
expect(process_service).to receive(:perform)
|
expect(process_service).to receive(:perform)
|
||||||
described_class.perform_now(params.with_indifferent_access)
|
described_class.perform_now(params.with_indifferent_access)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'calls Telegram::UpdateMessageService when the inbox is disabled' do
|
||||||
|
telegram_channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
process_service = double
|
||||||
|
allow(Telegram::UpdateMessageService).to receive(:new).and_return(process_service)
|
||||||
|
allow(process_service).to receive(:perform)
|
||||||
|
expect(Telegram::UpdateMessageService).to receive(:new).with(inbox: telegram_channel.inbox,
|
||||||
|
params: params['telegram'].with_indifferent_access)
|
||||||
|
expect(process_service).to receive(:perform)
|
||||||
|
described_class.perform_now(params.with_indifferent_access)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -42,6 +42,38 @@ RSpec.describe Webhooks::TiktokEventsJob do
|
|||||||
expect(read_status_service).to have_received(:perform)
|
expect(read_status_service).to have_received(:perform)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'processes im_mark_read_msg events when the inbox is disabled' do
|
||||||
|
channel.inbox.update!(active: false)
|
||||||
|
read_status_service = instance_double(Tiktok::ReadStatusService, perform: true)
|
||||||
|
allow(Tiktok::ReadStatusService).to receive(:new).and_return(read_status_service)
|
||||||
|
|
||||||
|
event = {
|
||||||
|
event: 'im_mark_read_msg',
|
||||||
|
user_openid: 'biz-123',
|
||||||
|
content: { conversation_id: 'tt-conv-1', read: { last_read_timestamp: 1_700_000_000_000 }, from_user: { id: 'user-1' } }.to_json
|
||||||
|
}
|
||||||
|
|
||||||
|
job.perform(event)
|
||||||
|
|
||||||
|
expect(Tiktok::ReadStatusService).to have_received(:new).with(channel: channel, content: hash_including(conversation_id: 'tt-conv-1'))
|
||||||
|
expect(read_status_service).to have_received(:perform)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not process im_receive_msg events when the inbox is disabled' do
|
||||||
|
channel.inbox.update!(active: false)
|
||||||
|
allow(Tiktok::MessageService).to receive(:new)
|
||||||
|
|
||||||
|
event = {
|
||||||
|
event: 'im_receive_msg',
|
||||||
|
user_openid: 'biz-123',
|
||||||
|
content: { conversation_id: 'tt-conv-1' }.to_json
|
||||||
|
}
|
||||||
|
|
||||||
|
job.perform(event)
|
||||||
|
|
||||||
|
expect(Tiktok::MessageService).not_to have_received(:new)
|
||||||
|
end
|
||||||
|
|
||||||
it 'ignores unsupported event types' do
|
it 'ignores unsupported event types' do
|
||||||
allow(Tiktok::MessageService).to receive(:new)
|
allow(Tiktok::MessageService).to receive(:new)
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,55 @@ RSpec.describe Webhooks::WhatsappEventsJob do
|
|||||||
job.perform_now(phone_number: unknown_phone)
|
job.perform_now(phone_number: unknown_phone)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'processes status callbacks when the inbox is disabled' do
|
||||||
|
wb_params = params.deep_dup
|
||||||
|
wb_params[:entry].first[:changes].first[:value][:statuses] = [
|
||||||
|
{ id: 'wamid-test', recipient_id: '919745786257', status: 'delivered' }
|
||||||
|
]
|
||||||
|
channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).and_return(process_service)
|
||||||
|
|
||||||
|
expect(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new).with(inbox: channel.inbox, params: wb_params)
|
||||||
|
|
||||||
|
job.perform_now(wb_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not process message callbacks when the inbox is disabled' do
|
||||||
|
wb_params = params.deep_dup
|
||||||
|
wb_params[:entry].first[:changes].first[:value][:messages] = [
|
||||||
|
{ from: '919745786257', id: 'wamid-test', text: { body: 'Hello' }, type: 'text' }
|
||||||
|
]
|
||||||
|
channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
allow(Whatsapp::IncomingMessageWhatsappCloudService).to receive(:new)
|
||||||
|
|
||||||
|
expect(Whatsapp::IncomingMessageWhatsappCloudService).not_to receive(:new)
|
||||||
|
|
||||||
|
job.perform_now(wb_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'processes call callbacks when the inbox is disabled' do
|
||||||
|
wb_params = params.deep_dup
|
||||||
|
wb_params[:entry].first[:changes].first[:field] = 'calls'
|
||||||
|
wb_params[:entry].first[:changes].first[:value][:calls] = [
|
||||||
|
{ id: 'wacid-test', from: '919745786257', event: 'connect', session: { sdp_type: 'offer' } }
|
||||||
|
]
|
||||||
|
channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
allow(Whatsapp::IncomingCallService).to receive(:new).and_return(process_service)
|
||||||
|
|
||||||
|
expect(Whatsapp::IncomingCallService).to receive(:new).with(
|
||||||
|
inbox: channel.inbox,
|
||||||
|
params: {
|
||||||
|
calls: [wb_params[:entry].first[:changes].first[:value][:calls].first],
|
||||||
|
contacts: nil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
job.perform_now(wb_params)
|
||||||
|
end
|
||||||
|
|
||||||
it 'uses from_user_id as the mutex sender for BSUID-only inbound messages' do
|
it 'uses from_user_id as the mutex sender for BSUID-only inbound messages' do
|
||||||
bsuid = 'IN.2081978709342942'
|
bsuid = 'IN.2081978709342942'
|
||||||
wb_params = params.deep_dup
|
wb_params = params.deep_dup
|
||||||
|
|||||||
@@ -52,6 +52,14 @@ describe CsatSurveyListener do
|
|||||||
|
|
||||||
listener.conversation_status_changed(event)
|
listener.conversation_status_changed(event)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not trigger CSAT survey service when the inbox is disabled' do
|
||||||
|
resolved_conversation.inbox.update!(active: false)
|
||||||
|
event = Events::Base.new(event_name, Time.zone.now, conversation: resolved_conversation)
|
||||||
|
|
||||||
|
expect(CsatSurveyService).not_to receive(:new)
|
||||||
|
listener.conversation_status_changed(event)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when conversation is not resolved' do
|
context 'when conversation is not resolved' do
|
||||||
|
|||||||
@@ -42,6 +42,17 @@ RSpec.describe ReplyMailbox do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with reply uuid present for a disabled inbox' do
|
||||||
|
before do
|
||||||
|
conversation.update!(uuid: '6bdc3f4d-0bec-4515-a284-5d916fdde489')
|
||||||
|
conversation.inbox.update!(active: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not add the mail content as a new message' do
|
||||||
|
expect { described_subject }.not_to change(Message, :count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with in reply to email' do
|
context 'with in reply to email' do
|
||||||
let(:reply_mail_without_uuid) { create_inbound_email_from_fixture('reply_mail_without_uuid.eml') }
|
let(:reply_mail_without_uuid) { create_inbound_email_from_fixture('reply_mail_without_uuid.eml') }
|
||||||
let(:described_subject) { described_class.receive reply_mail_without_uuid }
|
let(:described_subject) { described_class.receive reply_mail_without_uuid }
|
||||||
@@ -67,6 +78,27 @@ RSpec.describe ReplyMailbox do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when forwarded email inbox is disabled' do
|
||||||
|
let(:email_channel) { create(:channel_email, email: 'test@example.com', account: account) }
|
||||||
|
let(:forwarded_mail) { create_inbound_email_from_mail(from: 'sender@example.com', to: email_channel.email, subject: 'Hello') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
email_channel.inbox.update!(active: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create contacts, conversations, or messages' do
|
||||||
|
contact_count = Contact.count
|
||||||
|
conversation_count = Conversation.count
|
||||||
|
message_count = Message.count
|
||||||
|
|
||||||
|
described_class.receive forwarded_mail
|
||||||
|
|
||||||
|
expect(Contact.count).to eq(contact_count)
|
||||||
|
expect(Conversation.count).to eq(conversation_count)
|
||||||
|
expect(Message.count).to eq(message_count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when new conversation email contains null bytes' do
|
context 'when new conversation email contains null bytes' do
|
||||||
let(:email_channel) { create(:channel_email, email: 'test@example.com', account: account) }
|
let(:email_channel) { create(:channel_email, email: 'test@example.com', account: account) }
|
||||||
let(:null_byte_mail) { create_inbound_email_from_mail(from: 'sender@example.com', to: email_channel.email, subject: 'Hello') }
|
let(:null_byte_mail) { create_inbound_email_from_mail(from: 'sender@example.com', to: email_channel.email, subject: 'Hello') }
|
||||||
|
|||||||
@@ -142,6 +142,17 @@ RSpec.describe Campaign do
|
|||||||
campaign.trigger!
|
campaign.trigger!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not trigger the campaign when the inbox is disabled' do
|
||||||
|
campaign.save!
|
||||||
|
twilio_inbox.update!(active: false)
|
||||||
|
|
||||||
|
expect(Twilio::OneoffSmsCampaignService).not_to receive(:new)
|
||||||
|
|
||||||
|
campaign.trigger!
|
||||||
|
|
||||||
|
expect(campaign.reload.active?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
it 'keeps the campaign processing when triggering fails' do
|
it 'keeps the campaign processing when triggering fails' do
|
||||||
campaign.save!
|
campaign.save!
|
||||||
sms_service = double
|
sms_service = double
|
||||||
|
|||||||
@@ -252,6 +252,18 @@ describe Whatsapp::IncomingMessageService do
|
|||||||
expect(message.reload.status).to eq('read')
|
expect(message.reload.status).to eq('read')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'updates message status when the inbox is disabled' do
|
||||||
|
status_params = {
|
||||||
|
'statuses' => [{ 'recipient_id' => from, 'id' => from, 'status' => 'read' }]
|
||||||
|
}.with_indifferent_access
|
||||||
|
message = Message.find_by!(source_id: from)
|
||||||
|
whatsapp_channel.inbox.update!(active: false)
|
||||||
|
|
||||||
|
described_class.new(inbox: whatsapp_channel.inbox, params: status_params).perform
|
||||||
|
|
||||||
|
expect(message.reload.status).to eq('read')
|
||||||
|
end
|
||||||
|
|
||||||
it 'stores BSUID source ids from status contacts' do
|
it 'stores BSUID source ids from status contacts' do
|
||||||
bsuid = 'IN.2081978709342942'
|
bsuid = 'IN.2081978709342942'
|
||||||
parent_bsuid = 'IN.ENT.9081726354'
|
parent_bsuid = 'IN.ENT.9081726354'
|
||||||
|
|||||||
Reference in New Issue
Block a user