From cbd24dbdb20cca22853e84a1487628e117fd7de3 Mon Sep 17 00:00:00 2001 From: Muhsin <12408980+muhsin-k@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:55:41 +0400 Subject: [PATCH] feat(inboxes): add inbox disable toggle --- .../campaign_conversation_builder.rb | 2 ++ app/builders/conversation_builder.rb | 2 ++ .../messages/facebook/message_builder.rb | 1 + .../instagram/base_message_builder.rb | 1 + app/builders/messages/message_builder.rb | 5 +-- .../conversations/messages_controller.rb | 9 +++++ .../v1/accounts/conversations_controller.rb | 9 ++--- .../api/v1/accounts/inboxes_controller.rb | 2 +- .../api/v1/widget/base_controller.rb | 5 +++ .../api/v1/widget/conversations_controller.rb | 4 +++ .../concerns/request_exception_handler.rb | 8 +++++ .../platform/api/v1/inboxes_controller.rb | 34 +++++++++++++++++++ .../api/v1/inboxes/contacts_controller.rb | 5 +++ .../v1/inboxes/conversations_controller.rb | 5 +++ .../api/v1/inboxes/messages_controller.rb | 5 +++ app/controllers/widgets_controller.rb | 5 +++ .../Conversation/InboxName.vue | 8 ++++- .../components-next/sidebar/ChannelLeaf.vue | 11 ++++++ .../components/widgets/InboxName.vue | 8 ++++- .../widgets/conversation/MessagesView.vue | 14 +++++++- .../widgets/conversation/ReplyBox.vue | 7 ++++ .../i18n/locale/en/conversation.json | 2 ++ .../dashboard/i18n/locale/en/inboxMgmt.json | 5 +++ .../routes/dashboard/settings/inbox/Index.vue | 16 +++++++-- .../dashboard/settings/inbox/Settings.vue | 12 +++++++ .../inboxes/fetch_imap_email_inboxes_job.rb | 1 + app/jobs/inboxes/fetch_imap_emails_job.rb | 2 +- app/jobs/send_reply_job.rb | 2 ++ app/jobs/webhooks/instagram_events_job.rb | 1 + app/jobs/webhooks/line_events_job.rb | 1 + app/jobs/webhooks/sms_events_job.rb | 1 + app/jobs/webhooks/telegram_events_job.rb | 1 + app/jobs/webhooks/tiktok_events_job.rb | 1 + app/jobs/webhooks/whatsapp_events_job.rb | 1 + app/listeners/agent_bot_listener.rb | 2 ++ app/listeners/automation_rule_listener.rb | 10 ++++-- app/services/line/incoming_message_service.rb | 2 ++ app/services/sms/incoming_message_service.rb | 2 ++ .../telegram/incoming_message_service.rb | 2 ++ app/services/tiktok/message_service.rb | 2 ++ .../twilio/incoming_message_service.rb | 1 + .../twitter/direct_message_parser_service.rb | 2 ++ app/services/twitter/tweet_parser_service.rb | 1 + .../whatsapp/incoming_message_base_service.rb | 2 ++ app/views/api/v1/models/_inbox.json.jbuilder | 1 + config/routes.rb | 6 ++++ .../20260609090000_add_active_to_inboxes.rb | 7 ++++ db/schema.rb | 3 +- lib/custom_exceptions/inbox.rb | 19 +++++++++++ 49 files changed, 239 insertions(+), 19 deletions(-) create mode 100644 app/controllers/platform/api/v1/inboxes_controller.rb create mode 100644 db/migrate/20260609090000_add_active_to_inboxes.rb create mode 100644 lib/custom_exceptions/inbox.rb diff --git a/app/builders/campaigns/campaign_conversation_builder.rb b/app/builders/campaigns/campaign_conversation_builder.rb index 0e9b90105..dd0d23839 100644 --- a/app/builders/campaigns/campaign_conversation_builder.rb +++ b/app/builders/campaigns/campaign_conversation_builder.rb @@ -3,6 +3,8 @@ class Campaigns::CampaignConversationBuilder def perform @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) ActiveRecord::Base.transaction do diff --git a/app/builders/conversation_builder.rb b/app/builders/conversation_builder.rb index 6a995b188..07fa3dbc1 100644 --- a/app/builders/conversation_builder.rb +++ b/app/builders/conversation_builder.rb @@ -2,6 +2,8 @@ class ConversationBuilder pattr_initialize [:params!, :contact_inbox!] def perform + raise CustomExceptions::Inbox::Disabled unless @contact_inbox.inbox.active? + look_up_exising_conversation || create_new_conversation end diff --git a/app/builders/messages/facebook/message_builder.rb b/app/builders/messages/facebook/message_builder.rb index 24b6d9e70..17ae3db9f 100644 --- a/app/builders/messages/facebook/message_builder.rb +++ b/app/builders/messages/facebook/message_builder.rb @@ -20,6 +20,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder def perform # This channel might require reauthorization, may be owner might have changed the fb password return if @inbox.channel.reauthorization_required? + return unless @inbox.active? ActiveRecord::Base.transaction do build_contact_inbox diff --git a/app/builders/messages/instagram/base_message_builder.rb b/app/builders/messages/instagram/base_message_builder.rb index 8045e84c9..5665adcbe 100644 --- a/app/builders/messages/instagram/base_message_builder.rb +++ b/app/builders/messages/instagram/base_message_builder.rb @@ -10,6 +10,7 @@ class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuil def perform return if @inbox.channel.reauthorization_required? + return unless @inbox.active? ActiveRecord::Base.transaction do build_message diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index 297e77fcc..40a22f9da 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -22,14 +22,15 @@ class Messages::MessageBuilder end def perform + raise CustomExceptions::Inbox::Disabled unless @conversation.inbox.active? + @message = @conversation.messages.build(message_params) process_attachments process_emails # When the message has no quoted content, it will just be rendered as a regular message # The frontend is equipped to handle this case process_email_content - @message.save! - @message + @message.tap(&:save!) end private diff --git a/app/controllers/api/v1/accounts/conversations/messages_controller.rb b/app/controllers/api/v1/accounts/conversations/messages_controller.rb index 67381a715..d1ac68998 100644 --- a/app/controllers/api/v1/accounts/conversations/messages_controller.rb +++ b/app/controllers/api/v1/accounts/conversations/messages_controller.rb @@ -1,5 +1,6 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::Conversations::BaseController before_action :ensure_api_inbox, only: :update + before_action :ensure_inbox_active, only: [:create, :retry] def index @messages = message_finder.perform @@ -9,6 +10,8 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts:: user = Current.user || @resource mb = Messages::MessageBuilder.new(user, @conversation, params) @message = mb.perform + rescue CustomExceptions::Inbox::Disabled + render_inbox_disabled_error rescue StandardError => e render_could_not_create_error(e.message) end @@ -32,6 +35,8 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts:: service.perform message.update!(content_attributes: {}) ::SendReplyJob.perform_later(message.id) + rescue CustomExceptions::Inbox::Disabled + render_inbox_disabled_error rescue StandardError => e render_could_not_create_error(e.message) end @@ -77,4 +82,8 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts:: # Only API inboxes can update messages render json: { error: 'Message status update is only allowed for API inboxes' }, status: :forbidden unless @conversation.inbox.api? end + + def ensure_inbox_active + render_inbox_disabled_error unless @conversation.inbox.active? + end end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 2856c7817..e122a4f4d 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -197,6 +197,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro @inbox = Current.account.inboxes.find(params[:inbox_id]) authorize @inbox, :show? + render_inbox_disabled_error unless @inbox.active? end def contact @@ -213,6 +214,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro # and deprecate the support of passing only source_id as the param @contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id]) authorize @contact_inbox.inbox, :show? + render_inbox_disabled_error unless @contact_inbox.inbox.active? rescue ActiveRecord::RecordNotUnique render json: { error: 'source_id should be unique' }, status: :unprocessable_entity end @@ -220,12 +222,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro def build_contact_inbox return if @inbox.blank? || @contact.blank? - ContactInboxBuilder.new( - contact: @contact, - inbox: @inbox, - source_id: params[:source_id], - hmac_verified: hmac_verified? - ).perform + ContactInboxBuilder.new(contact: @contact, inbox: @inbox, source_id: params[:source_id], hmac_verified: hmac_verified?).perform end def conversation_finder diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 757af9b62..018f1f3d8 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -157,7 +157,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end 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, :lock_to_single_conversation, :portal_id, :sender_name_type, :business_name, { csat_config: [:display_type, :message, :button_text, :language, diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index 5b87e2d1a..ad43187be 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -3,6 +3,7 @@ class Api::V1::Widget::BaseController < ApplicationController include WebsiteTokenHelper before_action :set_web_widget + before_action :ensure_inbox_active before_action :set_contact private @@ -86,4 +87,8 @@ class Api::V1::Widget::BaseController < ApplicationController message_type: :incoming } end + + def ensure_inbox_active + render_inbox_disabled_error unless @web_widget.inbox.active? + end end diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 00e718614..881d393cd 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -1,6 +1,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController include Events::Types + + DISABLED_INBOX_ACTIONS = [:create, :toggle_typing, :toggle_status, :update_last_seen, :set_custom_attributes, :destroy_custom_attributes].freeze + 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 @conversation = conversation diff --git a/app/controllers/concerns/request_exception_handler.rb b/app/controllers/concerns/request_exception_handler.rb index ccab0090a..c078d0d5e 100644 --- a/app/controllers/concerns/request_exception_handler.rb +++ b/app/controllers/concerns/request_exception_handler.rb @@ -3,6 +3,7 @@ module RequestExceptionHandler included do rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid + rescue_from CustomExceptions::Inbox::Disabled, with: :render_inbox_disabled_error end private @@ -35,6 +36,13 @@ module RequestExceptionHandler render json: { error: message }, status: :unprocessable_entity 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) render json: { error: message }, status: :payment_required end diff --git a/app/controllers/platform/api/v1/inboxes_controller.rb b/app/controllers/platform/api/v1/inboxes_controller.rb new file mode 100644 index 000000000..bfb8812a5 --- /dev/null +++ b/app/controllers/platform/api/v1/inboxes_controller.rb @@ -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 diff --git a/app/controllers/public/api/v1/inboxes/contacts_controller.rb b/app/controllers/public/api/v1/inboxes/contacts_controller.rb index 835c2596b..72c148dd4 100644 --- a/app/controllers/public/api/v1/inboxes/contacts_controller.rb +++ b/app/controllers/public/api/v1/inboxes/contacts_controller.rb @@ -1,5 +1,6 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesController before_action :contact_inbox, except: [:create] + before_action :ensure_inbox_active, only: [:create, :update] before_action :process_hmac def show; end @@ -45,4 +46,8 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon def permitted_params params.permit(:identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {}) end + + def ensure_inbox_active + render_inbox_disabled_error unless @inbox_channel.inbox.active? + end end diff --git a/app/controllers/public/api/v1/inboxes/conversations_controller.rb b/app/controllers/public/api/v1/inboxes/conversations_controller.rb index 242dcde77..129d8c8dd 100644 --- a/app/controllers/public/api/v1/inboxes/conversations_controller.rb +++ b/app/controllers/public/api/v1/inboxes/conversations_controller.rb @@ -1,6 +1,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::InboxesController include Events::Types before_action :set_conversation, only: [:toggle_typing, :update_last_seen, :show, :toggle_status] + before_action :ensure_inbox_active, only: [:create, :toggle_typing, :toggle_status, :update_last_seen] def index @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 params.permit(custom_attributes: {}) end + + def ensure_inbox_active + render_inbox_disabled_error unless @contact_inbox.inbox.active? + end end diff --git a/app/controllers/public/api/v1/inboxes/messages_controller.rb b/app/controllers/public/api/v1/inboxes/messages_controller.rb index 4dc780fff..d3d27fbd5 100644 --- a/app/controllers/public/api/v1/inboxes/messages_controller.rb +++ b/app/controllers/public/api/v1/inboxes/messages_controller.rb @@ -1,5 +1,6 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesController before_action :set_message, only: [:update] + before_action :ensure_inbox_active, only: [:create, :update] def index @messages = @conversation.nil? ? [] : message_finder.perform @@ -70,4 +71,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon def check_csat_locked (Time.zone.now.to_date - @message.created_at.to_date).to_i > 14 and @message.content_type == 'input_csat' end + + def ensure_inbox_active + render_inbox_disabled_error unless @conversation.inbox.active? + end end diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index 913319303..c09988d6b 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -5,6 +5,7 @@ class WidgetsController < ActionController::Base before_action :set_global_config before_action :set_web_widget before_action :ensure_account_is_active + before_action :ensure_inbox_active before_action :ensure_location_is_supported before_action :set_token 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? end + def ensure_inbox_active + head :not_found unless @web_widget.inbox.active? + end + def ensure_location_is_supported; end def additional_attributes diff --git a/app/javascript/dashboard/components-next/Conversation/InboxName.vue b/app/javascript/dashboard/components-next/Conversation/InboxName.vue index c6a081c75..b7c91a5de 100644 --- a/app/javascript/dashboard/components-next/Conversation/InboxName.vue +++ b/app/javascript/dashboard/components-next/Conversation/InboxName.vue @@ -10,10 +10,16 @@ defineProps({ diff --git a/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue b/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue index a995cf510..e1b6b92d8 100644 --- a/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue +++ b/app/javascript/dashboard/components-next/sidebar/ChannelLeaf.vue @@ -27,6 +27,10 @@ const props = defineProps({ const reauthorizationRequired = computed(() => { return props.inbox.reauthorization_required; }); + +const inboxDisabled = computed(() => { + return props.inbox.active === false; +});