Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
class Api::V1::Accounts::AgentCapacityPolicies::InboxLimitsController < Api::V1::Accounts::EnterpriseAccountsController
|
||||
before_action -> { check_authorization(AgentCapacityPolicy) }
|
||||
before_action :fetch_policy
|
||||
before_action :fetch_inbox, only: [:create]
|
||||
before_action :fetch_inbox_limit, only: [:update, :destroy]
|
||||
before_action :validate_no_duplicate, only: [:create]
|
||||
|
||||
def create
|
||||
@inbox_limit = @agent_capacity_policy.inbox_capacity_limits.create!(
|
||||
inbox: @inbox,
|
||||
conversation_limit: permitted_params[:conversation_limit]
|
||||
)
|
||||
end
|
||||
|
||||
def update
|
||||
@inbox_limit.update!(conversation_limit: permitted_params[:conversation_limit])
|
||||
end
|
||||
|
||||
def destroy
|
||||
@inbox_limit.destroy!
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_policy
|
||||
@agent_capacity_policy = Current.account.agent_capacity_policies.find(params[:agent_capacity_policy_id])
|
||||
end
|
||||
|
||||
def fetch_inbox
|
||||
@inbox = Current.account.inboxes.find(permitted_params[:inbox_id])
|
||||
end
|
||||
|
||||
def fetch_inbox_limit
|
||||
@inbox_limit = @agent_capacity_policy.inbox_capacity_limits.find(params[:id])
|
||||
end
|
||||
|
||||
def validate_no_duplicate
|
||||
return unless @agent_capacity_policy.inbox_capacity_limits.exists?(inbox: @inbox)
|
||||
|
||||
render_could_not_create_error(I18n.t('agent_capacity_policy.inbox_already_assigned'))
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:inbox_id, :conversation_limit)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
class Api::V1::Accounts::AgentCapacityPolicies::UsersController < Api::V1::Accounts::EnterpriseAccountsController
|
||||
before_action -> { check_authorization(AgentCapacityPolicy) }
|
||||
before_action :fetch_policy
|
||||
before_action :fetch_user, only: [:destroy]
|
||||
|
||||
def index
|
||||
@users = User.joins(:account_users)
|
||||
.where(account_users: { account_id: Current.account.id, agent_capacity_policy_id: @agent_capacity_policy.id })
|
||||
end
|
||||
|
||||
def create
|
||||
@account_user = Current.account.account_users.find_by!(user_id: permitted_params[:user_id])
|
||||
@account_user.update!(agent_capacity_policy: @agent_capacity_policy)
|
||||
@user = @account_user.user
|
||||
end
|
||||
|
||||
def destroy
|
||||
@account_user.update!(agent_capacity_policy: nil)
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_policy
|
||||
@agent_capacity_policy = Current.account.agent_capacity_policies.find(params[:agent_capacity_policy_id])
|
||||
end
|
||||
|
||||
def fetch_user
|
||||
@account_user = Current.account.account_users.find_by!(user_id: params[:id])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:user_id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
class Api::V1::Accounts::AgentCapacityPoliciesController < Api::V1::Accounts::EnterpriseAccountsController
|
||||
before_action :check_authorization
|
||||
before_action :fetch_policy, only: [:show, :update, :destroy]
|
||||
|
||||
def index
|
||||
@agent_capacity_policies = Current.account.agent_capacity_policies
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@agent_capacity_policy = Current.account.agent_capacity_policies.create!(permitted_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@agent_capacity_policy.update!(permitted_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@agent_capacity_policy.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.require(:agent_capacity_policy).permit(
|
||||
:name,
|
||||
:description,
|
||||
exclusion_rules: [:exclude_older_than_hours, { excluded_labels: [] }]
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_policy
|
||||
@agent_capacity_policy = Current.account.agent_capacity_policies.find(params[:id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,70 @@
|
||||
class Api::V1::Accounts::AppliedSlasController < Api::V1::Accounts::EnterpriseAccountsController
|
||||
include Sift
|
||||
include DateRangeHelper
|
||||
|
||||
RESULTS_PER_PAGE = 25
|
||||
|
||||
before_action :set_applied_slas, only: [:index, :metrics, :download]
|
||||
before_action :set_current_page, only: [:index]
|
||||
before_action :check_admin_authorization?
|
||||
|
||||
sort_on :created_at, type: :datetime
|
||||
|
||||
def index
|
||||
@count = number_of_sla_misses
|
||||
@applied_slas = @missed_applied_slas.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
end
|
||||
|
||||
def metrics
|
||||
@total_applied_slas = total_applied_slas
|
||||
@number_of_sla_misses = number_of_sla_misses
|
||||
@hit_rate = hit_rate
|
||||
end
|
||||
|
||||
def download
|
||||
@missed_applied_slas = missed_applied_slas
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=breached_conversation.csv'
|
||||
render layout: false, formats: [:csv]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def total_applied_slas
|
||||
@total_applied_slas ||= @applied_slas.count
|
||||
end
|
||||
|
||||
def number_of_sla_misses
|
||||
@number_of_sla_misses ||= missed_applied_slas.count
|
||||
end
|
||||
|
||||
def hit_rate
|
||||
number_of_sla_misses.zero? ? '100%' : "#{hit_rate_percentage}%"
|
||||
end
|
||||
|
||||
def hit_rate_percentage
|
||||
((total_applied_slas - number_of_sla_misses) / total_applied_slas.to_f * 100).round(2)
|
||||
end
|
||||
|
||||
def set_applied_slas
|
||||
initial_query = Current.account.applied_slas.includes(:conversation)
|
||||
@applied_slas = apply_filters(initial_query)
|
||||
end
|
||||
|
||||
def apply_filters(query)
|
||||
query.filter_by_date_range(range)
|
||||
.filter_by_inbox_id(params[:inbox_id])
|
||||
.filter_by_team_id(params[:team_id])
|
||||
.filter_by_sla_policy_id(params[:sla_policy_id])
|
||||
.filter_by_label_list(params[:label_list])
|
||||
.filter_by_assigned_agent_id(params[:assigned_agent_id])
|
||||
end
|
||||
|
||||
def missed_applied_slas
|
||||
@missed_applied_slas ||= @applied_slas.missed
|
||||
end
|
||||
|
||||
def set_current_page
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
class Api::V1::Accounts::AuditLogsController < Api::V1::Accounts::EnterpriseAccountsController
|
||||
before_action :check_admin_authorization?
|
||||
before_action :fetch_audit
|
||||
|
||||
RESULTS_PER_PAGE = 15
|
||||
|
||||
def show
|
||||
@audit_logs = @audit_logs.page(params[:page]).per(RESULTS_PER_PAGE)
|
||||
@current_page = @audit_logs.current_page
|
||||
@total_entries = @audit_logs.total_count
|
||||
@per_page = RESULTS_PER_PAGE
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_audit
|
||||
@audit_logs = if audit_logs_enabled?
|
||||
Current.account.associated_audits.order(created_at: :desc)
|
||||
else
|
||||
Current.account.associated_audits.none
|
||||
end
|
||||
return if audit_logs_enabled?
|
||||
|
||||
Rails.logger.warn("Audit logs are disabled for account #{Current.account.id}")
|
||||
end
|
||||
|
||||
def audit_logs_enabled?
|
||||
Current.account.feature_enabled?(:audit_logs)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
class Api::V1::Accounts::Captain::AssistantResponsesController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action -> { check_authorization(Captain::Assistant) }
|
||||
|
||||
before_action :set_current_page, only: [:index]
|
||||
before_action :set_assistant, only: [:create]
|
||||
before_action :set_responses, except: [:create]
|
||||
before_action :set_response, only: [:show, :update, :destroy]
|
||||
|
||||
RESULTS_PER_PAGE = 25
|
||||
|
||||
def index
|
||||
filtered_query = apply_filters(@responses)
|
||||
@responses_count = filtered_query.count
|
||||
@responses = filtered_query.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@response = Current.account.captain_assistant_responses.new(response_params)
|
||||
@response.documentable = Current.user
|
||||
@response.save!
|
||||
end
|
||||
|
||||
def update
|
||||
@response.update!(response_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@response.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def apply_filters(base_query)
|
||||
base_query = base_query.where(assistant_id: permitted_params[:assistant_id]) if permitted_params[:assistant_id].present?
|
||||
|
||||
if permitted_params[:document_id].present?
|
||||
base_query = base_query.where(
|
||||
documentable_id: permitted_params[:document_id],
|
||||
documentable_type: 'Captain::Document'
|
||||
)
|
||||
end
|
||||
|
||||
base_query = base_query.where(status: permitted_params[:status]) if permitted_params[:status].present?
|
||||
|
||||
if permitted_params[:search].present?
|
||||
search_term = "%#{permitted_params[:search]}%"
|
||||
base_query = base_query.where(
|
||||
'question ILIKE :search OR answer ILIKE :search',
|
||||
search: search_term
|
||||
)
|
||||
end
|
||||
|
||||
base_query
|
||||
end
|
||||
|
||||
def set_assistant
|
||||
@assistant = Current.account.captain_assistants.find_by(id: params[:assistant_id])
|
||||
end
|
||||
|
||||
def set_responses
|
||||
@responses = Current.account.captain_assistant_responses.includes(:assistant, :documentable).ordered
|
||||
end
|
||||
|
||||
def set_response
|
||||
@response = @responses.find(permitted_params[:id])
|
||||
end
|
||||
|
||||
def set_current_page
|
||||
@current_page = permitted_params[:page] || 1
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :assistant_id, :page, :document_id, :account_id, :status, :search)
|
||||
end
|
||||
|
||||
def response_params
|
||||
params.require(:assistant_response).permit(
|
||||
:question,
|
||||
:answer,
|
||||
:assistant_id,
|
||||
:status
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,73 @@
|
||||
class Api::V1::Accounts::Captain::AssistantsController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action -> { check_authorization(Captain::Assistant) }
|
||||
|
||||
before_action :set_assistant, only: [:show, :update, :destroy, :playground]
|
||||
|
||||
def index
|
||||
@assistants = account_assistants.ordered
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@assistant = account_assistants.create!(assistant_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@assistant.update!(assistant_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@assistant.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def playground
|
||||
response = Captain::Llm::AssistantChatService.new(assistant: @assistant).generate_response(
|
||||
additional_message: params[:message_content],
|
||||
message_history: message_history
|
||||
)
|
||||
|
||||
render json: response
|
||||
end
|
||||
|
||||
def tools
|
||||
assistant = Captain::Assistant.new(account: Current.account)
|
||||
@tools = assistant.available_agent_tools
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_assistant
|
||||
@assistant = account_assistants.find(params[:id])
|
||||
end
|
||||
|
||||
def account_assistants
|
||||
@account_assistants ||= Captain::Assistant.for_account(Current.account.id)
|
||||
end
|
||||
|
||||
def assistant_params
|
||||
permitted = params.require(:assistant).permit(:name, :description,
|
||||
config: [
|
||||
:product_name, :feature_faq, :feature_memory, :feature_citation,
|
||||
:welcome_message, :handoff_message, :resolution_message,
|
||||
:instructions, :temperature
|
||||
])
|
||||
|
||||
# Handle array parameters separately to allow partial updates
|
||||
permitted[:response_guidelines] = params[:assistant][:response_guidelines] if params[:assistant].key?(:response_guidelines)
|
||||
|
||||
permitted[:guardrails] = params[:assistant][:guardrails] if params[:assistant].key?(:guardrails)
|
||||
|
||||
permitted
|
||||
end
|
||||
|
||||
def playground_params
|
||||
params.require(:assistant).permit(:message_content, message_history: [:role, :content])
|
||||
end
|
||||
|
||||
def message_history
|
||||
(playground_params[:message_history] || []).map { |message| { role: message[:role], content: message[:content] } }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
class Api::V1::Accounts::Captain::BulkActionsController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action -> { check_authorization(Captain::Assistant) }
|
||||
before_action :validate_params
|
||||
before_action :type_matches?
|
||||
|
||||
MODEL_TYPE = ['AssistantResponse'].freeze
|
||||
|
||||
def create
|
||||
@responses = process_bulk_action
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_params
|
||||
return if params[:type].present? && params[:ids].present? && params[:fields].present?
|
||||
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def type_matches?
|
||||
return if MODEL_TYPE.include?(params[:type])
|
||||
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def process_bulk_action
|
||||
case params[:type]
|
||||
when 'AssistantResponse'
|
||||
handle_assistant_responses
|
||||
end
|
||||
end
|
||||
|
||||
def handle_assistant_responses
|
||||
responses = Current.account.captain_assistant_responses.where(id: params[:ids])
|
||||
return unless responses.exists?
|
||||
|
||||
case params[:fields][:status]
|
||||
when 'approve'
|
||||
responses.pending.update(status: 'approved')
|
||||
responses
|
||||
when 'delete'
|
||||
responses.destroy_all
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:type, ids: [], fields: [:status])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
class Api::V1::Accounts::Captain::CopilotMessagesController < Api::V1::Accounts::BaseController
|
||||
before_action :set_copilot_thread
|
||||
|
||||
def index
|
||||
@copilot_messages = @copilot_thread
|
||||
.copilot_messages
|
||||
.includes(:copilot_thread)
|
||||
.order(created_at: :asc)
|
||||
.page(permitted_params[:page] || 1)
|
||||
.per(1000)
|
||||
end
|
||||
|
||||
def create
|
||||
@copilot_message = @copilot_thread.copilot_messages.create!(
|
||||
message: { content: params[:message] },
|
||||
message_type: :user
|
||||
)
|
||||
@copilot_message.enqueue_response_job(params[:conversation_id], Current.user.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_copilot_thread
|
||||
@copilot_thread = Current.account.copilot_threads.find_by!(
|
||||
id: params[:copilot_thread_id],
|
||||
user: Current.user
|
||||
)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:page)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
class Api::V1::Accounts::Captain::CopilotThreadsController < Api::V1::Accounts::BaseController
|
||||
before_action :ensure_message, only: :create
|
||||
|
||||
def index
|
||||
@copilot_threads = Current.account.copilot_threads
|
||||
.where(user_id: Current.user.id)
|
||||
.includes(:user, :assistant)
|
||||
.order(created_at: :desc)
|
||||
.page(permitted_params[:page] || 1)
|
||||
.per(5)
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
@copilot_thread = Current.account.copilot_threads.create!(
|
||||
title: copilot_thread_params[:message],
|
||||
user: Current.user,
|
||||
assistant: assistant
|
||||
)
|
||||
|
||||
copilot_message = @copilot_thread.copilot_messages.create!(
|
||||
message_type: :user,
|
||||
message: { content: copilot_thread_params[:message] }
|
||||
)
|
||||
|
||||
build_copilot_response(copilot_message)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_copilot_response(copilot_message)
|
||||
if Current.account.usage_limits[:captain][:responses][:current_available].positive?
|
||||
copilot_message.enqueue_response_job(copilot_thread_params[:conversation_id], Current.user.id)
|
||||
else
|
||||
copilot_message.copilot_thread.copilot_messages.create!(
|
||||
message_type: :assistant,
|
||||
message: { content: I18n.t('captain.copilot_limit') }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_message
|
||||
return render_could_not_create_error(I18n.t('captain.copilot_message_required')) if copilot_thread_params[:message].blank?
|
||||
end
|
||||
|
||||
def assistant
|
||||
Current.account.captain_assistants.find(copilot_thread_params[:assistant_id])
|
||||
end
|
||||
|
||||
def copilot_thread_params
|
||||
params.permit(:message, :assistant_id, :conversation_id)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:page)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
class Api::V1::Accounts::Captain::CustomToolsController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action -> { check_authorization(Captain::CustomTool) }
|
||||
before_action :set_custom_tool, only: [:show, :update, :destroy]
|
||||
|
||||
def index
|
||||
@custom_tools = account_custom_tools.enabled
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@custom_tool = account_custom_tools.create!(custom_tool_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@custom_tool.update!(custom_tool_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@custom_tool.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_custom_tool
|
||||
@custom_tool = account_custom_tools.find(params[:id])
|
||||
end
|
||||
|
||||
def account_custom_tools
|
||||
@account_custom_tools ||= Current.account.captain_custom_tools
|
||||
end
|
||||
|
||||
def custom_tool_params
|
||||
params.require(:custom_tool).permit(
|
||||
:title,
|
||||
:description,
|
||||
:endpoint_url,
|
||||
:http_method,
|
||||
:request_template,
|
||||
:response_template,
|
||||
:auth_type,
|
||||
:enabled,
|
||||
auth_config: {},
|
||||
param_schema: [:name, :type, :description, :required]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,62 @@
|
||||
class Api::V1::Accounts::Captain::DocumentsController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action -> { check_authorization(Captain::Assistant) }
|
||||
|
||||
before_action :set_current_page, only: [:index]
|
||||
before_action :set_documents, except: [:create]
|
||||
before_action :set_document, only: [:show, :destroy]
|
||||
before_action :set_assistant, only: [:create]
|
||||
RESULTS_PER_PAGE = 25
|
||||
|
||||
def index
|
||||
base_query = @documents
|
||||
base_query = base_query.where(assistant_id: permitted_params[:assistant_id]) if permitted_params[:assistant_id].present?
|
||||
|
||||
@documents_count = base_query.count
|
||||
@documents = base_query.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
return render_could_not_create_error('Missing Assistant') if @assistant.nil?
|
||||
|
||||
@document = @assistant.documents.build(document_params)
|
||||
@document.save!
|
||||
rescue Captain::Document::LimitExceededError => e
|
||||
render_could_not_create_error(e.message)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render_could_not_create_error(e.record.errors.full_messages.join(', '))
|
||||
end
|
||||
|
||||
def destroy
|
||||
@document.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_documents
|
||||
@documents = Current.account.captain_documents.includes(:assistant).ordered
|
||||
end
|
||||
|
||||
def set_document
|
||||
@document = @documents.find(permitted_params[:id])
|
||||
end
|
||||
|
||||
def set_assistant
|
||||
@assistant = Current.account.captain_assistants.find_by(id: document_params[:assistant_id])
|
||||
end
|
||||
|
||||
def set_current_page
|
||||
@current_page = permitted_params[:page] || 1
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:assistant_id, :page, :id, :account_id)
|
||||
end
|
||||
|
||||
def document_params
|
||||
params.require(:document).permit(:name, :external_link, :assistant_id, :pdf_file)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
class Api::V1::Accounts::Captain::InboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action -> { check_authorization(Captain::Assistant) }
|
||||
|
||||
before_action :set_assistant
|
||||
def index
|
||||
@inboxes = @assistant.inboxes
|
||||
end
|
||||
|
||||
def create
|
||||
inbox = Current.account.inboxes.find(assistant_params[:inbox_id])
|
||||
@captain_inbox = @assistant.captain_inboxes.build(inbox: inbox)
|
||||
@captain_inbox.save!
|
||||
end
|
||||
|
||||
def destroy
|
||||
@captain_inbox = @assistant.captain_inboxes.find_by!(inbox_id: permitted_params[:inbox_id])
|
||||
@captain_inbox.destroy!
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_assistant
|
||||
@assistant = account_assistants.find(permitted_params[:assistant_id])
|
||||
end
|
||||
|
||||
def account_assistants
|
||||
@account_assistants ||= Current.account.captain_assistants
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:assistant_id, :id, :account_id, :inbox_id)
|
||||
end
|
||||
|
||||
def assistant_params
|
||||
params.require(:inbox).permit(:inbox_id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
class Api::V1::Accounts::Captain::ScenariosController < Api::V1::Accounts::BaseController
|
||||
before_action :current_account
|
||||
before_action -> { check_authorization(Captain::Scenario) }
|
||||
before_action :set_assistant
|
||||
before_action :set_scenario, only: [:show, :update, :destroy]
|
||||
|
||||
def index
|
||||
@scenarios = assistant_scenarios.enabled
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@scenario = assistant_scenarios.create!(scenario_params.merge(account: Current.account))
|
||||
end
|
||||
|
||||
def update
|
||||
@scenario.update!(scenario_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@scenario.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_assistant
|
||||
@assistant = account_assistants.find(params[:assistant_id])
|
||||
end
|
||||
|
||||
def account_assistants
|
||||
@account_assistants ||= Current.account.captain_assistants
|
||||
end
|
||||
|
||||
def set_scenario
|
||||
@scenario = assistant_scenarios.find(params[:id])
|
||||
end
|
||||
|
||||
def assistant_scenarios
|
||||
@assistant.scenarios
|
||||
end
|
||||
|
||||
def scenario_params
|
||||
params.require(:scenario).permit(:title, :description, :instruction, :enabled, tools: [])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,71 @@
|
||||
class Api::V1::Accounts::Captain::TasksController < Api::V1::Accounts::BaseController
|
||||
before_action :check_authorization
|
||||
|
||||
def rewrite
|
||||
result = Captain::RewriteService.new(
|
||||
account: Current.account,
|
||||
content: params[:content],
|
||||
operation: params[:operation],
|
||||
conversation_display_id: params[:conversation_display_id]
|
||||
).perform
|
||||
|
||||
render_result(result)
|
||||
end
|
||||
|
||||
def summarize
|
||||
result = Captain::SummaryService.new(
|
||||
account: Current.account,
|
||||
conversation_display_id: params[:conversation_display_id]
|
||||
).perform
|
||||
|
||||
render_result(result)
|
||||
end
|
||||
|
||||
def reply_suggestion
|
||||
result = Captain::ReplySuggestionService.new(
|
||||
account: Current.account,
|
||||
conversation_display_id: params[:conversation_display_id],
|
||||
user: Current.user
|
||||
).perform
|
||||
|
||||
render_result(result)
|
||||
end
|
||||
|
||||
def label_suggestion
|
||||
result = Captain::LabelSuggestionService.new(
|
||||
account: Current.account,
|
||||
conversation_display_id: params[:conversation_display_id]
|
||||
).perform
|
||||
|
||||
render_result(result)
|
||||
end
|
||||
|
||||
def follow_up
|
||||
result = Captain::FollowUpService.new(
|
||||
account: Current.account,
|
||||
follow_up_context: params[:follow_up_context]&.to_unsafe_h,
|
||||
user_message: params[:message],
|
||||
conversation_display_id: params[:conversation_display_id]
|
||||
).perform
|
||||
|
||||
render_result(result)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_result(result)
|
||||
if result.nil?
|
||||
render json: { message: nil }
|
||||
elsif result[:error]
|
||||
render json: { error: result[:error] }, status: :unprocessable_entity
|
||||
else
|
||||
response_data = { message: result[:message] }
|
||||
response_data[:follow_up_context] = result[:follow_up_context] if result[:follow_up_context]
|
||||
render json: response_data
|
||||
end
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(:'captain/tasks')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
class Api::V1::Accounts::CompaniesController < Api::V1::Accounts::EnterpriseAccountsController
|
||||
include Sift
|
||||
sort_on :name, type: :string
|
||||
sort_on :domain, type: :string
|
||||
sort_on :created_at, type: :datetime
|
||||
sort_on :contacts_count, internal_name: :order_on_contacts_count, type: :scope, scope_params: [:direction]
|
||||
|
||||
RESULTS_PER_PAGE = 25
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :set_current_page, only: [:index, :search]
|
||||
before_action :fetch_company, only: [:show, :update, :destroy]
|
||||
|
||||
def index
|
||||
@companies = fetch_companies(resolved_companies)
|
||||
@companies_count = @companies.total_count
|
||||
end
|
||||
|
||||
def search
|
||||
if params[:q].blank?
|
||||
return render json: { error: I18n.t('errors.companies.search.query_missing') },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
|
||||
companies = resolved_companies.search_by_name_or_domain(params[:q])
|
||||
@companies = fetch_companies(companies)
|
||||
@companies_count = @companies.total_count
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@company = Current.account.companies.build(company_params)
|
||||
@company.save!
|
||||
end
|
||||
|
||||
def update
|
||||
@company.update!(company_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@company.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolved_companies
|
||||
@resolved_companies ||= Current.account.companies
|
||||
end
|
||||
|
||||
def set_current_page
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
|
||||
def fetch_companies(companies)
|
||||
filtrate(companies)
|
||||
.page(@current_page)
|
||||
.per(RESULTS_PER_PAGE)
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless ChatwootApp.enterprise?
|
||||
|
||||
authorize(Company)
|
||||
end
|
||||
|
||||
def fetch_company
|
||||
@company = Current.account.companies.find(params[:id])
|
||||
end
|
||||
|
||||
def company_params
|
||||
params.require(:company).permit(:name, :domain, :description, :avatar)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
class Api::V1::Accounts::ConferenceController < Api::V1::Accounts::BaseController
|
||||
before_action :set_voice_inbox_for_conference
|
||||
|
||||
def token
|
||||
render json: Voice::Provider::Twilio::TokenService.new(
|
||||
inbox: @voice_inbox,
|
||||
user: Current.user,
|
||||
account: Current.account
|
||||
).generate
|
||||
end
|
||||
|
||||
def create
|
||||
conversation = fetch_conversation_by_display_id
|
||||
ensure_call_sid!(conversation)
|
||||
|
||||
conference_service = Voice::Provider::Twilio::ConferenceService.new(conversation: conversation)
|
||||
conference_sid = conference_service.ensure_conference_sid
|
||||
conference_service.mark_agent_joined(user: current_user)
|
||||
|
||||
render json: {
|
||||
status: 'success',
|
||||
id: conversation.display_id,
|
||||
conference_sid: conference_sid,
|
||||
using_webrtc: true
|
||||
}
|
||||
end
|
||||
|
||||
def destroy
|
||||
conversation = fetch_conversation_by_display_id
|
||||
Voice::Provider::Twilio::ConferenceService.new(conversation: conversation).end_conference
|
||||
render json: { status: 'success', id: conversation.display_id }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_call_sid!(conversation)
|
||||
return conversation.identifier if conversation.identifier.present?
|
||||
|
||||
incoming_sid = params.require(:call_sid)
|
||||
|
||||
conversation.update!(identifier: incoming_sid)
|
||||
incoming_sid
|
||||
end
|
||||
|
||||
def set_voice_inbox_for_conference
|
||||
@voice_inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
authorize @voice_inbox, :show?
|
||||
end
|
||||
|
||||
def fetch_conversation_by_display_id
|
||||
cid = params[:conversation_id]
|
||||
raise ActiveRecord::RecordNotFound, 'conversation_id required' if cid.blank?
|
||||
|
||||
conversation = @voice_inbox.conversations.find_by!(display_id: cid)
|
||||
authorize conversation, :show?
|
||||
conversation
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
class Api::V1::Accounts::Contacts::CallsController < Api::V1::Accounts::BaseController
|
||||
before_action :contact
|
||||
before_action :voice_inbox
|
||||
|
||||
def create
|
||||
authorize contact, :show?
|
||||
authorize voice_inbox, :show?
|
||||
|
||||
result = Voice::OutboundCallBuilder.perform!(
|
||||
account: Current.account,
|
||||
inbox: voice_inbox,
|
||||
user: Current.user,
|
||||
contact: contact
|
||||
)
|
||||
|
||||
conversation = result[:conversation]
|
||||
|
||||
render json: {
|
||||
conversation_id: conversation.display_id,
|
||||
inbox_id: voice_inbox.id,
|
||||
call_sid: result[:call_sid],
|
||||
conference_sid: conversation.additional_attributes['conference_sid']
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def contact
|
||||
@contact ||= Current.account.contacts.find(params[:id])
|
||||
end
|
||||
|
||||
def voice_inbox
|
||||
@voice_inbox ||= Current.user.assigned_inboxes.where(
|
||||
account_id: Current.account.id,
|
||||
channel_type: 'Channel::Voice'
|
||||
).find(params.require(:inbox_id))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
class Api::V1::Accounts::CustomRolesController < Api::V1::Accounts::EnterpriseAccountsController
|
||||
before_action :fetch_custom_role, only: [:show, :update, :destroy]
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
@custom_roles = Current.account.custom_roles
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@custom_role = Current.account.custom_roles.create!(permitted_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@custom_role.update!(permitted_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@custom_role.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.require(:custom_role).permit(:name, :description, permissions: [])
|
||||
end
|
||||
|
||||
def fetch_custom_role
|
||||
@custom_role = Current.account.custom_roles.find_by(id: params[:id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
class Api::V1::Accounts::EnterpriseAccountsController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
class Api::V1::Accounts::ReportingEventsController < Api::V1::Accounts::EnterpriseAccountsController
|
||||
include DateRangeHelper
|
||||
|
||||
RESULTS_PER_PAGE = 25
|
||||
|
||||
before_action :check_admin_authorization?
|
||||
before_action :set_reporting_events, only: [:index]
|
||||
before_action :set_current_page, only: [:index]
|
||||
|
||||
def index
|
||||
@reporting_events = @reporting_events.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
@total_count = @reporting_events.total_count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_reporting_events
|
||||
@reporting_events = Current.account.reporting_events
|
||||
.includes(:conversation, :user, :inbox)
|
||||
.filter_by_date_range(range)
|
||||
.filter_by_inbox_id(params[:inbox_id])
|
||||
.filter_by_user_id(params[:user_id])
|
||||
.filter_by_name(params[:name])
|
||||
.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def set_current_page
|
||||
@current_page = (params[:page] || 1).to_i
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,63 @@
|
||||
class Api::V1::Accounts::SamlSettingsController < Api::V1::Accounts::BaseController
|
||||
before_action :check_saml_sso_enabled
|
||||
before_action :check_saml_feature_enabled
|
||||
before_action :check_authorization
|
||||
before_action :set_saml_settings
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@saml_settings = Current.account.build_saml_settings(saml_settings_params)
|
||||
if @saml_settings.save
|
||||
render :show
|
||||
else
|
||||
render json: { errors: @saml_settings.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
if @saml_settings.update(saml_settings_params)
|
||||
render :show
|
||||
else
|
||||
render json: { errors: @saml_settings.errors.full_messages }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@saml_settings.destroy!
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_saml_settings
|
||||
@saml_settings = Current.account.saml_settings ||
|
||||
Current.account.build_saml_settings
|
||||
end
|
||||
|
||||
def saml_settings_params
|
||||
params.require(:saml_settings).permit(
|
||||
:sso_url,
|
||||
:certificate,
|
||||
:idp_entity_id,
|
||||
:sp_entity_id,
|
||||
role_mappings: {}
|
||||
)
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(AccountSamlSettings)
|
||||
end
|
||||
|
||||
def check_saml_feature_enabled
|
||||
return if Current.account.feature_enabled?('saml')
|
||||
|
||||
render json: { error: I18n.t('errors.saml.feature_not_enabled') }, status: :forbidden
|
||||
end
|
||||
|
||||
def check_saml_sso_enabled
|
||||
return if GlobalConfigService.load('ENABLE_SAML_SSO_LOGIN', 'true').to_s == 'true'
|
||||
|
||||
render json: { error: I18n.t('errors.saml.sso_not_enabled') }, status: :forbidden
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
class Api::V1::Accounts::SlaPoliciesController < Api::V1::Accounts::EnterpriseAccountsController
|
||||
before_action :fetch_sla, only: [:show, :update, :destroy]
|
||||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
@sla_policies = Current.account.sla_policies
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@sla_policy = Current.account.sla_policies.create!(permitted_params)
|
||||
end
|
||||
|
||||
def update
|
||||
@sla_policy.update!(permitted_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
::DeleteObjectJob.perform_later(@sla_policy, Current.user, request.ip) if @sla_policy.present?
|
||||
head :ok
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.require(:sla_policy).permit(:name, :description, :first_response_time_threshold, :next_response_time_threshold,
|
||||
:resolution_time_threshold, :only_during_business_hours)
|
||||
end
|
||||
|
||||
def fetch_sla
|
||||
@sla_policy = Current.account.sla_policies.find_by(id: params[:id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
class Api::V1::AuthController < Api::BaseController
|
||||
skip_before_action :authenticate_user!, only: [:saml_login]
|
||||
before_action :find_user_and_account, only: [:saml_login]
|
||||
|
||||
def saml_login
|
||||
unless saml_sso_enabled?
|
||||
render json: { error: 'SAML SSO login is not enabled' }, status: :forbidden
|
||||
return
|
||||
end
|
||||
|
||||
return if @account.nil?
|
||||
|
||||
relay_state = params[:target] || 'web'
|
||||
|
||||
saml_initiation_url = "/auth/saml?account_id=#{@account.id}&RelayState=#{relay_state}"
|
||||
redirect_to saml_initiation_url, status: :temporary_redirect
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_user_and_account
|
||||
return unless validate_email_presence
|
||||
|
||||
find_saml_enabled_account
|
||||
end
|
||||
|
||||
def validate_email_presence
|
||||
@email = params[:email]&.downcase&.strip
|
||||
return true if @email.present?
|
||||
|
||||
render json: { error: I18n.t('auth.saml.invalid_email') }, status: :bad_request
|
||||
false
|
||||
end
|
||||
|
||||
def find_saml_enabled_account
|
||||
user = User.from_email(@email)
|
||||
return render_saml_error unless user
|
||||
|
||||
account_user = find_account_with_saml(user)
|
||||
return render_saml_error unless account_user
|
||||
|
||||
@account = account_user.account
|
||||
end
|
||||
|
||||
def find_account_with_saml(user)
|
||||
user.account_users
|
||||
.joins(account: :saml_settings)
|
||||
.where.not(saml_settings: { sso_url: [nil, ''] })
|
||||
.where.not(saml_settings: { certificate: [nil, ''] })
|
||||
.find { |account_user| account_user.account.feature_enabled?('saml') }
|
||||
end
|
||||
|
||||
def render_saml_error
|
||||
error = 'saml-authentication-failed'
|
||||
|
||||
if mobile_target?
|
||||
mobile_deep_link_base = GlobalConfigService.load('MOBILE_DEEP_LINK_BASE', 'chatwootapp')
|
||||
redirect_to "#{mobile_deep_link_base}://auth/saml?error=#{ERB::Util.url_encode(error)}", allow_other_host: true
|
||||
else
|
||||
redirect_to sso_login_page_url(error: error)
|
||||
end
|
||||
end
|
||||
|
||||
def mobile_target?
|
||||
params[:target]&.casecmp('mobile')&.zero?
|
||||
end
|
||||
|
||||
def sso_login_page_url(error: nil)
|
||||
frontend_url = ENV.fetch('FRONTEND_URL', nil)
|
||||
params = { error: error }.compact
|
||||
|
||||
query = params.to_query
|
||||
query_fragment = query.present? ? "?#{query}" : ''
|
||||
|
||||
"#{frontend_url}/app/login/sso#{query_fragment}"
|
||||
end
|
||||
|
||||
def saml_sso_enabled?
|
||||
GlobalConfigService.load('ENABLE_SAML_SSO_LOGIN', 'true').to_s == 'true'
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
class CustomDomainsController < ApplicationController
|
||||
def verify
|
||||
challenge_id = permitted_params[:id]
|
||||
|
||||
domain = request.host
|
||||
portal = Portal.find_by(custom_domain: domain)
|
||||
|
||||
return render plain: 'Domain not found', status: :not_found unless portal
|
||||
|
||||
ssl_settings = portal.ssl_settings || {}
|
||||
|
||||
return render plain: 'Challenge ID not found', status: :not_found unless ssl_settings['cf_verification_id'] == challenge_id
|
||||
|
||||
render plain: ssl_settings['cf_verification_body'], status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
module Enterprise::Api::V1::Accounts::AgentsController
|
||||
def create
|
||||
super
|
||||
associate_agent_with_custom_role
|
||||
end
|
||||
|
||||
def update
|
||||
super
|
||||
associate_agent_with_custom_role
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def associate_agent_with_custom_role
|
||||
@agent.current_account_user.update!(custom_role_id: params[:custom_role_id])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
module Enterprise::Api::V1::Accounts::ConversationsController
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def inbox_assistant
|
||||
assistant = @conversation.inbox.captain_assistant
|
||||
|
||||
if assistant
|
||||
render json: { assistant: { id: assistant.id, name: assistant.name } }
|
||||
else
|
||||
render json: { assistant: nil }
|
||||
end
|
||||
end
|
||||
|
||||
def reporting_events
|
||||
@reporting_events = @conversation.reporting_events.order(created_at: :asc)
|
||||
end
|
||||
|
||||
def permitted_update_params
|
||||
super.merge(params.permit(:sla_policy_id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def copilot_params
|
||||
params.permit(:previous_history, :message, :assistant_id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
module Enterprise::Api::V1::Accounts::CsatSurveyResponsesController
|
||||
def update
|
||||
@csat_survey_response = Current.account.csat_survey_responses.find(params[:id])
|
||||
authorize @csat_survey_response
|
||||
|
||||
@csat_survey_response.update!(
|
||||
csat_review_notes: params[:csat_review_notes],
|
||||
review_notes_updated_by: Current.user,
|
||||
review_notes_updated_at: Time.current
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
module Enterprise::Api::V1::Accounts::InboxesController
|
||||
def inbox_attributes
|
||||
super + ee_inbox_attributes
|
||||
end
|
||||
|
||||
def ee_inbox_attributes
|
||||
[auto_assignment_config: [:max_assignment_limit]]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allowed_channel_types
|
||||
super + ['voice']
|
||||
end
|
||||
|
||||
def channel_type_from_params
|
||||
case permitted_params[:channel][:type]
|
||||
when 'voice'
|
||||
Channel::Voice
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def account_channels_method
|
||||
case permitted_params[:channel][:type]
|
||||
when 'voice'
|
||||
Current.account.voice_channels
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
module Enterprise::Api::V1::Accounts::PortalsController
|
||||
def ssl_status
|
||||
return render_could_not_create_error(I18n.t('portals.ssl_status.custom_domain_not_configured')) if @portal.custom_domain.blank?
|
||||
|
||||
result = Cloudflare::CheckCustomHostnameService.new(portal: @portal).perform
|
||||
|
||||
return render_could_not_create_error(result[:errors]) if result[:errors].present?
|
||||
|
||||
ssl_settings = @portal.ssl_settings || {}
|
||||
render json: {
|
||||
status: ssl_settings['cf_status'],
|
||||
verification_errors: ssl_settings['cf_verification_errors']
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,147 @@
|
||||
class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
include BillingHelper
|
||||
before_action :fetch_account
|
||||
before_action :check_authorization
|
||||
before_action :check_cloud_env, only: [:limits, :toggle_deletion]
|
||||
|
||||
def subscription
|
||||
if stripe_customer_id.blank? && @account.custom_attributes['is_creating_customer'].blank?
|
||||
@account.update(custom_attributes: { is_creating_customer: true })
|
||||
Enterprise::CreateStripeCustomerJob.perform_later(@account)
|
||||
end
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def limits
|
||||
limits = if default_plan?(@account)
|
||||
{
|
||||
'conversation' => {
|
||||
'allowed' => 500,
|
||||
'consumed' => conversations_this_month(@account)
|
||||
},
|
||||
'non_web_inboxes' => {
|
||||
'allowed' => 0,
|
||||
'consumed' => non_web_inboxes(@account)
|
||||
},
|
||||
'agents' => {
|
||||
'allowed' => 2,
|
||||
'consumed' => agents(@account)
|
||||
}
|
||||
}
|
||||
else
|
||||
default_limits
|
||||
end
|
||||
|
||||
# include id in response to ensure that the store can be updated on the frontend
|
||||
render json: { id: @account.id, limits: limits }, status: :ok
|
||||
end
|
||||
|
||||
def checkout
|
||||
return create_stripe_billing_session(stripe_customer_id) if stripe_customer_id.present?
|
||||
|
||||
render_invalid_billing_details
|
||||
end
|
||||
|
||||
def toggle_deletion
|
||||
action_type = params[:action_type]
|
||||
|
||||
case action_type
|
||||
when 'delete'
|
||||
mark_for_deletion
|
||||
when 'undelete'
|
||||
unmark_for_deletion
|
||||
else
|
||||
render json: { error: 'Invalid action_type. Must be either "delete" or "undelete"' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def topup_checkout
|
||||
return render json: { error: I18n.t('errors.topup.credits_required') }, status: :unprocessable_entity if params[:credits].blank?
|
||||
|
||||
service = Enterprise::Billing::TopupCheckoutService.new(account: @account)
|
||||
result = service.create_checkout_session(credits: params[:credits].to_i)
|
||||
|
||||
@account.reload
|
||||
render json: result.merge(
|
||||
id: @account.id,
|
||||
limits: @account.limits,
|
||||
custom_attributes: @account.custom_attributes
|
||||
)
|
||||
rescue Enterprise::Billing::TopupCheckoutService::Error, Stripe::StripeError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_cloud_env
|
||||
render json: { error: 'Not found' }, status: :not_found unless ChatwootApp.chatwoot_cloud?
|
||||
end
|
||||
|
||||
def default_limits
|
||||
{
|
||||
'conversation' => {},
|
||||
'non_web_inboxes' => {},
|
||||
'agents' => {
|
||||
'allowed' => @account.usage_limits[:agents],
|
||||
'consumed' => agents(@account)
|
||||
},
|
||||
'captain' => @account.usage_limits[:captain]
|
||||
}
|
||||
end
|
||||
|
||||
def fetch_account
|
||||
@account = current_user.accounts.find(params[:id])
|
||||
@current_account_user = @account.account_users.find_by(user_id: current_user.id)
|
||||
end
|
||||
|
||||
def stripe_customer_id
|
||||
@account.custom_attributes['stripe_customer_id']
|
||||
end
|
||||
|
||||
def mark_for_deletion
|
||||
reason = 'manual_deletion'
|
||||
|
||||
if @account.mark_for_deletion(reason)
|
||||
cancel_cloud_subscriptions_for_deletion
|
||||
|
||||
render json: { message: 'Account marked for deletion' }, status: :ok
|
||||
else
|
||||
render json: { message: @account.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def unmark_for_deletion
|
||||
if @account.unmark_for_deletion
|
||||
render json: { message: 'Account unmarked for deletion' }, status: :ok
|
||||
else
|
||||
render json: { message: @account.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def render_invalid_billing_details
|
||||
render_could_not_create_error('Please subscribe to a plan before viewing the billing details')
|
||||
end
|
||||
|
||||
def create_stripe_billing_session(customer_id)
|
||||
session = Enterprise::Billing::CreateSessionService.new.create_session(customer_id)
|
||||
render_redirect_url(session.url)
|
||||
end
|
||||
|
||||
def cancel_cloud_subscriptions_for_deletion
|
||||
Enterprise::Billing::CancelCloudSubscriptionsService.new(account: @account).perform
|
||||
rescue Stripe::StripeError => e
|
||||
Rails.logger.warn("Failed to cancel cloud subscriptions for account #{@account.id}: #{e.class} - #{e.message}")
|
||||
end
|
||||
|
||||
def render_redirect_url(redirect_url)
|
||||
render json: { redirect_url: redirect_url }
|
||||
end
|
||||
|
||||
def pundit_user
|
||||
{
|
||||
user: current_user,
|
||||
account: @account,
|
||||
account_user: @current_account_user
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
module Enterprise::Api::V1::AccountsSettings
|
||||
private
|
||||
|
||||
def permitted_settings_attributes
|
||||
super + [{ conversation_required_attributes: [] }]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
module Enterprise::Api::V2::AccountsController
|
||||
private
|
||||
|
||||
def fetch_account_and_user_info
|
||||
@data = fetch_from_clearbit
|
||||
|
||||
return if @data.blank?
|
||||
|
||||
update_user_info
|
||||
end
|
||||
|
||||
def fetch_from_clearbit
|
||||
Enterprise::ClearbitLookupService.lookup(@user.email)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error fetching data from clearbit: #{e}"
|
||||
nil
|
||||
end
|
||||
|
||||
def update_user_info
|
||||
@user.update!(name: @data[:name]) if @data[:name].present?
|
||||
end
|
||||
|
||||
def data_from_clearbit
|
||||
return {} if @data.blank?
|
||||
|
||||
{ name: @data[:company_name],
|
||||
custom_attributes: {
|
||||
'industry' => @data[:industry],
|
||||
'company_size' => @data[:company_size],
|
||||
'timezone' => @data[:timezone],
|
||||
'logo' => @data[:logo]
|
||||
} }
|
||||
end
|
||||
|
||||
def account_attributes
|
||||
super.deep_merge(
|
||||
data_from_clearbit
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,3 @@
|
||||
module Enterprise::Concerns::ApplicationControllerConcern
|
||||
extend ActiveSupport::Concern
|
||||
end
|
||||
@@ -0,0 +1,100 @@
|
||||
module Enterprise::DeviseOverrides::OmniauthCallbacksController
|
||||
def saml
|
||||
# Call parent's omniauth_success which handles the auth
|
||||
omniauth_success
|
||||
end
|
||||
|
||||
def redirect_callbacks
|
||||
# derive target redirect route from 'resource_class' param, which was set
|
||||
# before authentication.
|
||||
devise_mapping = get_devise_mapping
|
||||
redirect_route = get_redirect_route(devise_mapping)
|
||||
|
||||
# preserve omniauth info for success route. ignore 'extra' in twitter
|
||||
# auth response to avoid CookieOverflow.
|
||||
session['dta.omniauth.auth'] = request.env['omniauth.auth'].except('extra')
|
||||
session['dta.omniauth.params'] = request.env['omniauth.params']
|
||||
|
||||
# For SAML, use 303 See Other to convert POST to GET and preserve session
|
||||
if params[:provider] == 'saml'
|
||||
redirect_to redirect_route, { status: 303 }.merge(redirect_options)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def omniauth_success
|
||||
case auth_hash&.dig('provider')
|
||||
when 'saml'
|
||||
handle_saml_auth
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def omniauth_failure
|
||||
return super unless params[:provider] == 'saml'
|
||||
|
||||
relay_state = saml_relay_state
|
||||
error = params[:message] || 'authentication-failed'
|
||||
|
||||
if for_mobile?(relay_state)
|
||||
redirect_to_mobile_error(error, relay_state)
|
||||
else
|
||||
redirect_to login_page_url(error: "saml-#{error}")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_saml_auth
|
||||
account_id = extract_saml_account_id
|
||||
relay_state = saml_relay_state
|
||||
|
||||
unless saml_enabled_for_account?(account_id)
|
||||
return redirect_to_mobile_error('saml-not-enabled') if for_mobile?(relay_state)
|
||||
|
||||
return redirect_to login_page_url(error: 'saml-not-enabled')
|
||||
end
|
||||
|
||||
@resource = SamlUserBuilder.new(auth_hash, account_id).perform
|
||||
|
||||
if @resource.persisted?
|
||||
return sign_in_user_on_mobile if for_mobile?(relay_state)
|
||||
|
||||
sign_in_user
|
||||
else
|
||||
return redirect_to_mobile_error('saml-authentication-failed') if for_mobile?(relay_state)
|
||||
|
||||
redirect_to login_page_url(error: 'saml-authentication-failed')
|
||||
end
|
||||
end
|
||||
|
||||
def extract_saml_account_id
|
||||
params[:account_id] || session[:saml_account_id] || request.env['omniauth.params']&.dig('account_id')
|
||||
end
|
||||
|
||||
def saml_relay_state
|
||||
session[:saml_relay_state] || request.env['omniauth.params']&.dig('RelayState')
|
||||
end
|
||||
|
||||
def for_mobile?(relay_state)
|
||||
relay_state.to_s.casecmp('mobile').zero?
|
||||
end
|
||||
|
||||
def redirect_to_mobile_error(error)
|
||||
mobile_deep_link_base = GlobalConfigService.load('MOBILE_DEEP_LINK_BASE', 'chatwootapp')
|
||||
redirect_to "#{mobile_deep_link_base}://auth/saml?error=#{ERB::Util.url_encode(error)}", allow_other_host: true
|
||||
end
|
||||
|
||||
def saml_enabled_for_account?(account_id)
|
||||
return false if account_id.blank?
|
||||
|
||||
account = Account.find_by(id: account_id)
|
||||
|
||||
return false if account.nil?
|
||||
return false unless account.feature_enabled?('saml')
|
||||
|
||||
AccountSamlSettings.find_by(account_id: account_id).present?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
module Enterprise::DeviseOverrides::PasswordsController
|
||||
include SamlAuthenticationHelper
|
||||
|
||||
def create
|
||||
if saml_user_attempting_password_auth?(params[:email])
|
||||
render json: {
|
||||
success: false,
|
||||
message: I18n.t('messages.reset_password_saml_user'),
|
||||
errors: [I18n.t('messages.reset_password_saml_user')]
|
||||
}, status: :forbidden
|
||||
return
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
module Enterprise::DeviseOverrides::SessionsController
|
||||
include SamlAuthenticationHelper
|
||||
|
||||
def create
|
||||
if saml_user_attempting_password_auth?(params[:email], sso_auth_token: params[:sso_auth_token])
|
||||
render json: {
|
||||
success: false,
|
||||
message: I18n.t('messages.login_saml_user'),
|
||||
errors: [I18n.t('messages.login_saml_user')]
|
||||
}, status: :unauthorized
|
||||
return
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def render_create_success
|
||||
create_audit_event('sign_in')
|
||||
super
|
||||
end
|
||||
|
||||
def destroy
|
||||
create_audit_event('sign_out')
|
||||
super
|
||||
end
|
||||
|
||||
def create_audit_event(action)
|
||||
return unless @resource
|
||||
|
||||
associated_type = 'Account'
|
||||
@resource.accounts.each do |account|
|
||||
@resource.audits.create(
|
||||
action: action,
|
||||
user_id: @resource.id,
|
||||
associated_id: account.id,
|
||||
associated_type: associated_type
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
module Enterprise::Public::Api::V1::Portals::ArticlesController
|
||||
private
|
||||
|
||||
def search_articles
|
||||
if @portal.account.feature_enabled?('help_center_embedding_search')
|
||||
@articles = @articles.vector_search(list_params.merge(account_id: @portal.account_id)) if list_params[:query].present?
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
module Enterprise::SuperAdmin::AccountsController
|
||||
def update
|
||||
# Handle manually managed features from form submission
|
||||
if params[:account] && params[:account][:manually_managed_features].present?
|
||||
# Update using the service - it will handle array conversion and validation
|
||||
service = ::Internal::Accounts::InternalAttributesService.new(requested_resource)
|
||||
service.manually_managed_features = params[:account][:manually_managed_features]
|
||||
|
||||
# Remove the manually_managed_features from params to prevent ActiveModel::UnknownAttributeError
|
||||
params[:account].delete(:manually_managed_features)
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
module Enterprise::SuperAdmin::AppConfigsController
|
||||
private
|
||||
|
||||
def allowed_configs
|
||||
return super if ChatwootHub.pricing_plan == 'community'
|
||||
|
||||
case @config
|
||||
when 'custom_branding'
|
||||
@allowed_configs = custom_branding_options
|
||||
when 'internal'
|
||||
@allowed_configs = internal_config_options
|
||||
when 'captain'
|
||||
@allowed_configs = captain_config_options
|
||||
when 'saml'
|
||||
@allowed_configs = saml_config_options
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def custom_branding_options
|
||||
%w[
|
||||
LOGO_THUMBNAIL
|
||||
LOGO
|
||||
LOGO_DARK
|
||||
BRAND_NAME
|
||||
INSTALLATION_NAME
|
||||
BRAND_URL
|
||||
WIDGET_BRAND_URL
|
||||
TERMS_URL
|
||||
PRIVACY_URL
|
||||
DISPLAY_MANIFEST
|
||||
]
|
||||
end
|
||||
|
||||
def internal_config_options
|
||||
%w[CHATWOOT_INBOX_TOKEN CHATWOOT_INBOX_HMAC_KEY CLOUD_ANALYTICS_TOKEN CLEARBIT_API_KEY DASHBOARD_SCRIPTS INACTIVE_WHATSAPP_NUMBERS
|
||||
SKIP_INCOMING_BCC_PROCESSING CAPTAIN_CLOUD_PLAN_LIMITS ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL CHATWOOT_INSTANCE_ADMIN_EMAIL
|
||||
OG_IMAGE_CDN_URL OG_IMAGE_CLIENT_REF CLOUDFLARE_API_KEY CLOUDFLARE_ZONE_ID BLOCKED_EMAIL_DOMAINS
|
||||
OTEL_PROVIDER LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY LANGFUSE_BASE_URL]
|
||||
end
|
||||
|
||||
def captain_config_options
|
||||
%w[
|
||||
CAPTAIN_OPEN_AI_API_KEY
|
||||
CAPTAIN_OPEN_AI_MODEL
|
||||
CAPTAIN_OPEN_AI_ENDPOINT
|
||||
CAPTAIN_EMBEDDING_MODEL
|
||||
CAPTAIN_FIRECRAWL_API_KEY
|
||||
]
|
||||
end
|
||||
|
||||
def saml_config_options
|
||||
%w[ENABLE_SAML_SSO_LOGIN]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
class Enterprise::Webhooks::FirecrawlController < ActionController::API
|
||||
before_action :validate_token
|
||||
|
||||
def process_payload
|
||||
Captain::Tools::FirecrawlParserJob.perform_later(assistant_id: assistant.id, payload: payload) if crawl_page_event?
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
include Captain::FirecrawlHelper
|
||||
|
||||
def payload
|
||||
permitted_params[:data]&.first&.to_h
|
||||
end
|
||||
|
||||
def validate_token
|
||||
render json: { error: 'Invalid access_token' }, status: :unauthorized if assistant_token != permitted_params[:token]
|
||||
end
|
||||
|
||||
def assistant
|
||||
@assistant ||= Captain::Assistant.find(permitted_params[:assistant_id])
|
||||
end
|
||||
|
||||
def assistant_token
|
||||
generate_firecrawl_token(assistant.id, assistant.account_id)
|
||||
end
|
||||
|
||||
def crawl_page_event?
|
||||
permitted_params[:type] == 'crawl.page'
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(
|
||||
:type,
|
||||
:assistant_id,
|
||||
:token,
|
||||
:success,
|
||||
:id,
|
||||
:metadata,
|
||||
:format,
|
||||
:firecrawl,
|
||||
data: [:markdown, { metadata: {} }]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
class Enterprise::Webhooks::StripeController < ActionController::API
|
||||
def process_payload
|
||||
# Get the event payload and signature
|
||||
payload = request.body.read
|
||||
sig_header = request.headers['Stripe-Signature']
|
||||
|
||||
# Attempt to verify the signature. If successful, we'll handle the event
|
||||
begin
|
||||
event = Stripe::Webhook.construct_event(payload, sig_header, ENV.fetch('STRIPE_WEBHOOK_SECRET', nil))
|
||||
::Enterprise::Billing::HandleStripeEventService.new.perform(event: event)
|
||||
# If we fail to verify the signature, then something was wrong with the request
|
||||
rescue JSON::ParserError, Stripe::SignatureVerificationError
|
||||
# Invalid payload
|
||||
head :bad_request
|
||||
return
|
||||
end
|
||||
|
||||
# We've successfully processed the event without blowing up
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
module Enterprise::WidgetsController
|
||||
private
|
||||
|
||||
def ensure_location_is_supported
|
||||
countries = @web_widget.inbox.account.custom_attributes['allowed_countries']
|
||||
return if countries.blank?
|
||||
|
||||
geocoder_result = IpLookupService.new.perform(request.remote_ip)
|
||||
return unless geocoder_result
|
||||
|
||||
country_enabled = countries.include?(geocoder_result.country_code)
|
||||
render json: { error: 'Location is not supported' }, status: :unauthorized unless country_enabled
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,2 @@
|
||||
class SuperAdmin::EnterpriseBaseController < SuperAdmin::ApplicationController
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
class SuperAdmin::ResponseDocumentsController < SuperAdmin::EnterpriseBaseController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
# def update
|
||||
# super
|
||||
# send_foo_updated_email(requested_resource)
|
||||
# end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes(action_name)).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
class SuperAdmin::ResponsesController < SuperAdmin::EnterpriseBaseController
|
||||
# Overwrite any of the RESTful controller actions to implement custom behavior
|
||||
# For example, you may want to send an email after a foo is updated.
|
||||
#
|
||||
# def update
|
||||
# super
|
||||
# send_foo_updated_email(requested_resource)
|
||||
# end
|
||||
|
||||
# Override this method to specify custom lookup behavior.
|
||||
# This will be used to set the resource for the `show`, `edit`, and `update`
|
||||
# actions.
|
||||
#
|
||||
# def find_resource(param)
|
||||
# Foo.find_by!(slug: param)
|
||||
# end
|
||||
|
||||
# The result of this lookup will be available as `requested_resource`
|
||||
|
||||
# Override this if you have certain roles that require a subset
|
||||
# this will be used to set the records shown on the `index` action.
|
||||
#
|
||||
# def scoped_resource
|
||||
# if current_user.super_admin?
|
||||
# resource_class
|
||||
# else
|
||||
# resource_class.with_less_stuff
|
||||
# end
|
||||
# end
|
||||
|
||||
# Override `resource_params` if you want to transform the submitted
|
||||
# data before it's persisted. For example, the following would turn all
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
# def resource_params
|
||||
# params.require(resource_class.model_name.param_key).
|
||||
# permit(dashboard.permitted_attributes(action_name)).
|
||||
# transform_values { |value| value == "" ? nil : value }
|
||||
# end
|
||||
|
||||
# See https://administrate-demo.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
end
|
||||
@@ -0,0 +1,188 @@
|
||||
class Twilio::VoiceController < ApplicationController
|
||||
CONFERENCE_EVENT_PATTERNS = {
|
||||
/conference-start/i => 'start',
|
||||
/participant-join/i => 'join',
|
||||
/participant-leave/i => 'leave',
|
||||
/conference-end/i => 'end'
|
||||
}.freeze
|
||||
|
||||
before_action :set_inbox!
|
||||
|
||||
def status
|
||||
Voice::StatusUpdateService.new(
|
||||
account: current_account,
|
||||
call_sid: twilio_call_sid,
|
||||
call_status: params[:CallStatus],
|
||||
payload: params.to_unsafe_h
|
||||
).perform
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def call_twiml
|
||||
account = current_account
|
||||
Rails.logger.info(
|
||||
"TWILIO_VOICE_TWIML account=#{account.id} call_sid=#{twilio_call_sid} from=#{twilio_from} direction=#{twilio_direction}"
|
||||
)
|
||||
|
||||
conversation = resolve_conversation
|
||||
conference_sid = ensure_conference_sid!(conversation)
|
||||
|
||||
render xml: conference_twiml(conference_sid, agent_leg?(twilio_from))
|
||||
end
|
||||
|
||||
def conference_status
|
||||
event = mapped_conference_event
|
||||
return head :no_content unless event
|
||||
|
||||
conversation = find_conversation_for_conference!(
|
||||
friendly_name: params[:FriendlyName],
|
||||
call_sid: twilio_call_sid
|
||||
)
|
||||
|
||||
Voice::Conference::Manager.new(
|
||||
conversation: conversation,
|
||||
event: event,
|
||||
call_sid: twilio_call_sid,
|
||||
participant_label: participant_label
|
||||
).process
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def twilio_call_sid
|
||||
params[:CallSid]
|
||||
end
|
||||
|
||||
def twilio_from
|
||||
params[:From].to_s
|
||||
end
|
||||
|
||||
def twilio_to
|
||||
params[:To]
|
||||
end
|
||||
|
||||
def twilio_direction
|
||||
@twilio_direction ||= (params['Direction'] || params['CallDirection']).to_s
|
||||
end
|
||||
|
||||
def mapped_conference_event
|
||||
event = params[:StatusCallbackEvent].to_s
|
||||
CONFERENCE_EVENT_PATTERNS.each do |pattern, mapped|
|
||||
return mapped if event.match?(pattern)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def agent_leg?(from_number)
|
||||
from_number.start_with?('client:')
|
||||
end
|
||||
|
||||
def resolve_conversation
|
||||
return find_conversation_for_agent if agent_leg?(twilio_from)
|
||||
|
||||
case twilio_direction
|
||||
when 'inbound'
|
||||
Voice::InboundCallBuilder.perform!(
|
||||
account: current_account,
|
||||
inbox: inbox,
|
||||
from_number: twilio_from,
|
||||
call_sid: twilio_call_sid
|
||||
)
|
||||
when 'outbound-api', 'outbound-dial'
|
||||
sync_outbound_leg(
|
||||
call_sid: twilio_call_sid,
|
||||
from_number: twilio_from,
|
||||
direction: twilio_direction
|
||||
)
|
||||
else
|
||||
raise ArgumentError, "Unsupported Twilio direction: #{twilio_direction}"
|
||||
end
|
||||
end
|
||||
|
||||
def find_conversation_for_agent
|
||||
if params[:conversation_id].present?
|
||||
current_account.conversations.find_by!(display_id: params[:conversation_id])
|
||||
else
|
||||
current_account.conversations.find_by!(identifier: twilio_call_sid)
|
||||
end
|
||||
end
|
||||
|
||||
def sync_outbound_leg(call_sid:, from_number:, direction:)
|
||||
parent_sid = params['ParentCallSid'].presence
|
||||
lookup_sid = direction == 'outbound-dial' ? parent_sid || call_sid : call_sid
|
||||
conversation = current_account.conversations.find_by!(identifier: lookup_sid)
|
||||
|
||||
Voice::CallSessionSyncService.new(
|
||||
conversation: conversation,
|
||||
call_sid: call_sid,
|
||||
message_call_sid: conversation.identifier,
|
||||
leg: {
|
||||
from_number: from_number,
|
||||
to_number: twilio_to,
|
||||
direction: 'outbound'
|
||||
}
|
||||
).perform
|
||||
end
|
||||
|
||||
def ensure_conference_sid!(conversation)
|
||||
attrs = conversation.additional_attributes || {}
|
||||
attrs['conference_sid'] ||= Voice::Conference::Name.for(conversation)
|
||||
conversation.update!(additional_attributes: attrs)
|
||||
attrs['conference_sid']
|
||||
end
|
||||
|
||||
def conference_twiml(conference_sid, agent_leg)
|
||||
Twilio::TwiML::VoiceResponse.new.tap do |response|
|
||||
response.dial do |dial|
|
||||
dial.conference(
|
||||
conference_sid,
|
||||
start_conference_on_enter: agent_leg,
|
||||
end_conference_on_exit: false,
|
||||
status_callback: conference_status_callback_url,
|
||||
status_callback_event: 'start end join leave',
|
||||
status_callback_method: 'POST',
|
||||
participant_label: agent_leg ? 'agent' : 'contact'
|
||||
)
|
||||
end
|
||||
end.to_s
|
||||
end
|
||||
|
||||
def conference_status_callback_url
|
||||
phone_digits = inbox_channel.phone_number.delete_prefix('+')
|
||||
Rails.application.routes.url_helpers.twilio_voice_conference_status_url(phone: phone_digits)
|
||||
end
|
||||
|
||||
def find_conversation_for_conference!(friendly_name:, call_sid:)
|
||||
name = friendly_name.to_s
|
||||
scope = current_account.conversations
|
||||
|
||||
if name.present?
|
||||
conversation = scope.where("additional_attributes->>'conference_sid' = ?", name).first
|
||||
return conversation if conversation
|
||||
end
|
||||
|
||||
scope.find_by!(identifier: call_sid)
|
||||
end
|
||||
|
||||
def set_inbox!
|
||||
digits = params[:phone].to_s.gsub(/\D/, '')
|
||||
e164 = "+#{digits}"
|
||||
channel = Channel::Voice.find_by!(phone_number: e164)
|
||||
@inbox = channel.inbox
|
||||
end
|
||||
|
||||
def current_account
|
||||
@current_account ||= inbox_account
|
||||
end
|
||||
|
||||
def participant_label
|
||||
params[:ParticipantLabel].to_s
|
||||
end
|
||||
|
||||
attr_reader :inbox
|
||||
|
||||
delegate :account, :channel, to: :inbox, prefix: true
|
||||
end
|
||||
Reference in New Issue
Block a user