Merge remote-tracking branch 'origin/assignment_v2/assignment_service' into assignment_v2/assignment_service

This commit is contained in:
Tanmay Deep Sharma
2025-11-12 18:00:34 +05:30
1665 changed files with 104331 additions and 7317 deletions
+2 -2
View File
@@ -4,7 +4,7 @@
# 3. Automation Filters (app/services/automation_rules/conditions_filter_service.rb), (app/services/automation_rules/condition_validation_service.rb)
# Format
# Format
# - Parent Key (conversation, contact, messages)
# - Key (attribute_name)
# - attribute_type: "standard" : supported ["standard", "additional_attributes (only for conversations and messages)"]
@@ -138,7 +138,7 @@ contacts:
- "does_not_contain"
phone_number:
attribute_type: "standard"
data_type: "text_case_insensitive"
data_type: "text" # Text is not explicity defined in filters, default filter will be used
filter_operators:
- "equal_to"
- "not_equal_to"
+1 -1
View File
@@ -81,7 +81,7 @@ class Integrations::OpenaiBaseService
end
def api_url
endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value || 'https://api.openai.com/'
endpoint = InstallationConfig.find_by(name: 'CAPTAIN_OPEN_AI_ENDPOINT')&.value.presence || 'https://api.openai.com/'
endpoint = endpoint.chomp('/')
"#{endpoint}/v1/chat/completions"
end
+21 -3
View File
@@ -24,13 +24,31 @@ class Integrations::Slack::ChannelBuilder
end
def channels
conversations_list = slack_client.conversations_list(types: 'public_channel,private_channel', exclude_archived: true)
# Split channel fetching into separate API calls to avoid rate limiting issues.
# Slack's API handles single-type requests (public OR private) much more efficiently
# than mixed-type requests (public AND private). This approach eliminates rate limits
# that occur when requesting both channel types simultaneously.
channel_list = []
# Step 1: Fetch all private channels in one call (expect very few)
private_channels = fetch_channels_by_type('private_channel')
channel_list.concat(private_channels)
# Step 2: Fetch public channels with pagination
public_channels = fetch_channels_by_type('public_channel')
channel_list.concat(public_channels)
channel_list
end
def fetch_channels_by_type(channel_type, limit: 1000)
conversations_list = slack_client.conversations_list(types: channel_type, exclude_archived: true, limit: limit)
channel_list = conversations_list.channels
while conversations_list.response_metadata.next_cursor.present?
conversations_list = slack_client.conversations_list(
cursor: conversations_list.response_metadata.next_cursor,
types: 'public_channel,private_channel',
exclude_archived: true
types: channel_type,
exclude_archived: true,
limit: limit
)
channel_list.concat(conversations_list.channels)
end
+25 -26
View File
@@ -101,7 +101,7 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
def send_message
post_message if message_content.present?
upload_file if message.attachments.any?
upload_files if message.attachments.any?
rescue Slack::Web::Api::Errors::AccountInactive, Slack::Web::Api::Errors::MissingScope, Slack::Web::Api::Errors::InvalidAuth,
Slack::Web::Api::Errors::ChannelNotFound, Slack::Web::Api::Errors::NotInChannel => e
Rails.logger.error e
@@ -120,36 +120,35 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
)
end
def upload_file
message.attachments.each do |attachment|
next unless attachment.with_attached_file?
def upload_files
return unless message.attachments.any?
begin
result = slack_client.files_upload_v2(
filename: attachment.file.filename.to_s,
content: attachment.file.download,
initial_comment: 'Attached File!',
thread_ts: conversation.identifier,
channel_id: hook.reference_id
)
Rails.logger.info "slack_upload_result: #{result}"
rescue Slack::Web::Api::Errors::SlackError => e
Rails.logger.error "Failed to upload file #{attachment.file.filename}: #{e.message}"
end
files = build_files_array
return if files.empty?
begin
result = slack_client.files_upload_v2(
files: files,
initial_comment: 'Attached File!',
thread_ts: conversation.identifier,
channel_id: hook.reference_id
)
Rails.logger.info "slack_upload_result: #{result}"
rescue Slack::Web::Api::Errors::SlackError => e
Rails.logger.error "Failed to upload files: #{e.message}"
end
end
def file_type
File.extname(message.attachments.first.download_url).strip.downcase[1..]
end
def build_files_array
message.attachments.filter_map do |attachment|
next unless attachment.with_attached_file?
def file_information
{
filename: message.attachments.first.file.filename,
filetype: file_type,
content: message.attachments.first.file.download,
title: message.attachments.first.file.filename
}
{
filename: attachment.file.filename.to_s,
content: attachment.file.download,
title: attachment.file.filename.to_s
}
end
end
def sender_name(sender)
@@ -70,7 +70,9 @@ module Integrations::Slack::SlackMessageHelper
case attachment[:filetype]
when 'png', 'jpeg', 'gif', 'bmp', 'tiff', 'jpg'
:image
when 'pdf'
when 'mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'
:video
else
:file
end
end
+3
View File
@@ -6,6 +6,9 @@ module Limits
GREETING_MESSAGE_MAX_LENGTH = 10_000
CATEGORIES_PER_PAGE = 1000
AUTO_ASSIGNMENT_BULK_LIMIT = 100
COMPANY_NAME_LENGTH_LIMIT = 100
COMPANY_DESCRIPTION_LENGTH_LIMIT = 1000
MAX_CUSTOM_FILTERS_PER_USER = 1000
def self.conversation_message_per_minute_limit
ENV.fetch('CONVERSATION_MESSAGE_PER_MINUTE_LIMIT', '200').to_i
+25 -11
View File
@@ -16,8 +16,11 @@ class Seeders::Reports::ConversationCreator
@priorities = [nil, 'urgent', 'high', 'medium', 'low']
end
# rubocop:disable Metrics/MethodLength
def create_conversation(created_at:)
conversation = nil
should_resolve = false
resolution_time = nil
ActiveRecord::Base.transaction do
travel_to(created_at) do
@@ -26,14 +29,35 @@ class Seeders::Reports::ConversationCreator
add_labels_to_conversation(conversation)
create_messages_for_conversation(conversation)
resolve_conversation_if_needed(conversation)
# Determine if should resolve but don't update yet
should_resolve = rand > 0.3
if should_resolve
resolution_delay = rand((30.minutes)..(24.hours))
resolution_time = created_at + resolution_delay
end
end
travel_back
end
# Now resolve outside of time travel if needed
if should_resolve && resolution_time
# rubocop:disable Rails/SkipsModelValidations
conversation.update_column(:status, :resolved)
conversation.update_column(:updated_at, resolution_time)
# rubocop:enable Rails/SkipsModelValidations
# Trigger the event with proper timestamp
travel_to(resolution_time) do
trigger_conversation_resolved_event(conversation)
end
travel_back
end
conversation
end
# rubocop:enable Metrics/MethodLength
private
@@ -85,16 +109,6 @@ class Seeders::Reports::ConversationCreator
message_creator.create_messages
end
def resolve_conversation_if_needed(conversation)
return unless rand < 0.7
resolution_delay = rand((30.minutes)..(24.hours))
travel(resolution_delay)
conversation.update!(status: :resolved)
trigger_conversation_resolved_event(conversation)
end
def trigger_conversation_resolved_event(conversation)
event_data = { conversation: conversation }
+1 -1
View File
@@ -118,7 +118,7 @@ class CaptainChatSession
end
def show_available_tools
available_tools = Captain::Assistant.available_tool_ids
available_tools = @assistant.available_tool_ids
if available_tools.any?
puts "🔧 Available Tools (#{available_tools.count}): #{available_tools.join(', ')}"
else
+12
View File
@@ -0,0 +1,12 @@
namespace :companies do
desc 'Backfill companies from existing contact email domains'
task backfill: :environment do
puts 'Starting company backfill migration...'
puts 'This will process all accounts and create companies from contact email domains.'
puts 'The job will run in the background via Sidekiq'
puts ''
Migration::CompanyBackfillJob.perform_later
puts 'Company backfill job has been enqueued.'
puts 'Monitor progress in logs or Sidekiq dashboard.'
end
end
+65
View File
@@ -0,0 +1,65 @@
module MfaTasks
def self.find_user_or_exit(email)
abort 'Error: Please provide an email address' if email.blank?
user = User.from_email(email)
abort "Error: User with email '#{email}' not found" unless user
user
end
def self.reset_user_mfa(user)
user.update!(
otp_required_for_login: false,
otp_secret: nil,
otp_backup_codes: nil
)
end
def self.reset_single(args)
user = find_user_or_exit(args[:email])
abort "MFA is already disabled for #{args[:email]}" if !user.otp_required_for_login? && user.otp_secret.nil?
reset_user_mfa(user)
puts "✓ MFA has been successfully reset for #{args[:email]}"
rescue StandardError => e
abort "Error resetting MFA: #{e.message}"
end
def self.reset_all
print 'Are you sure you want to reset MFA for ALL users? This cannot be undone! (yes/no): '
abort 'Operation cancelled' unless $stdin.gets.chomp.downcase == 'yes'
affected_users = User.where(otp_required_for_login: true).or(User.where.not(otp_secret: nil))
count = affected_users.count
abort 'No users have MFA enabled' if count.zero?
puts "\nResetting MFA for #{count} user(s)..."
affected_users.find_each { |user| reset_user_mfa(user) }
puts "✓ MFA has been reset for #{count} user(s)"
end
def self.generate_backup_codes(args)
user = find_user_or_exit(args[:email])
abort "Error: MFA is not enabled for #{args[:email]}" unless user.otp_required_for_login?
service = Mfa::ManagementService.new(user: user)
codes = service.generate_backup_codes!
puts "\nNew backup codes generated for #{args[:email]}:"
codes.each { |code| puts code }
end
end
namespace :mfa do
desc 'Reset MFA for a specific user by email'
task :reset, [:email] => :environment do |_task, args|
MfaTasks.reset_single(args)
end
desc 'Reset MFA for all users in the system'
task reset_all: :environment do
MfaTasks.reset_all
end
desc 'Generate new backup codes for a user'
task :generate_backup_codes, [:email] => :environment do |_task, args|
MfaTasks.generate_backup_codes(args)
end
end
+23 -4
View File
@@ -31,14 +31,33 @@ class Webhooks::Trigger
end
def handle_error(error)
return unless should_handle_error?
return unless SUPPORTED_ERROR_HANDLE_EVENTS.include?(@payload[:event])
return unless message
update_message_status(error)
case @webhook_type
when :agent_bot_webhook
conversation = message.conversation
return unless conversation&.pending?
conversation.open!
create_agent_bot_error_activity(conversation)
when :api_inbox_webhook
update_message_status(error)
end
end
def should_handle_error?
@webhook_type == :api_inbox_webhook && SUPPORTED_ERROR_HANDLE_EVENTS.include?(@payload[:event])
def create_agent_bot_error_activity(conversation)
content = I18n.t('conversations.activity.agent_bot.error_moved_to_open')
Conversations::ActivityMessageJob.perform_later(conversation, activity_message_params(conversation, content))
end
def activity_message_params(conversation, content)
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :activity,
content: content
}
end
def update_message_status(error)