Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
class AutomationRules::ActionService < ActionService
|
||||
def initialize(rule, account, conversation)
|
||||
super(conversation)
|
||||
@rule = rule
|
||||
@account = account
|
||||
Current.executed_by = rule
|
||||
end
|
||||
|
||||
def perform
|
||||
@rule.actions.each do |action|
|
||||
@conversation.reload
|
||||
action = action.with_indifferent_access
|
||||
begin
|
||||
send(action[:action_name], action[:action_params])
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e, account: @account).capture_exception
|
||||
end
|
||||
end
|
||||
ensure
|
||||
Current.reset
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_attachment(blob_ids)
|
||||
return if conversation_a_tweet?
|
||||
|
||||
return unless @rule.files.attached?
|
||||
|
||||
blobs = ActiveStorage::Blob.where(id: blob_ids)
|
||||
|
||||
return if blobs.blank?
|
||||
|
||||
params = { content: nil, private: false, attachments: blobs }
|
||||
Messages::MessageBuilder.new(nil, @conversation, params).perform
|
||||
end
|
||||
|
||||
def send_webhook_event(webhook_url)
|
||||
payload = @conversation.webhook_data.merge(event: "automation_event.#{@rule.event_name}")
|
||||
WebhookJob.perform_later(webhook_url[0], payload)
|
||||
end
|
||||
|
||||
def send_message(message)
|
||||
return if conversation_a_tweet?
|
||||
|
||||
params = { content: message[0], private: false, content_attributes: { automation_rule_id: @rule.id } }
|
||||
Messages::MessageBuilder.new(nil, @conversation, params).perform
|
||||
end
|
||||
|
||||
def add_private_note(message)
|
||||
return if conversation_a_tweet?
|
||||
|
||||
params = { content: message[0], private: true, content_attributes: { automation_rule_id: @rule.id } }
|
||||
Messages::MessageBuilder.new(nil, @conversation.reload, params).perform
|
||||
end
|
||||
|
||||
def send_email_to_team(params)
|
||||
teams = Team.where(id: params[0][:team_ids])
|
||||
|
||||
teams.each do |team|
|
||||
break unless @account.within_email_rate_limit?
|
||||
|
||||
TeamNotifications::AutomationNotificationMailer.conversation_creation(@conversation, team, params[0][:message])&.deliver_now
|
||||
@account.increment_email_sent_count
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
class AutomationRules::ConditionValidationService
|
||||
ATTRIBUTE_MODEL = 'conversation_attribute'.freeze
|
||||
|
||||
def initialize(rule)
|
||||
@rule = rule
|
||||
@account = rule.account
|
||||
|
||||
file = File.read('./lib/filters/filter_keys.yml')
|
||||
@filters = YAML.safe_load(file)
|
||||
|
||||
@conversation_filters = @filters['conversations']
|
||||
@contact_filters = @filters['contacts']
|
||||
@message_filters = @filters['messages']
|
||||
end
|
||||
|
||||
def perform
|
||||
@rule.conditions.each do |condition|
|
||||
return false unless valid_condition?(condition) && valid_query_operator?(condition)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_query_operator?(condition)
|
||||
query_operator = condition['query_operator']
|
||||
|
||||
return true if query_operator.nil?
|
||||
return true if query_operator.empty?
|
||||
|
||||
%w[AND OR].include?(query_operator.upcase)
|
||||
end
|
||||
|
||||
def valid_condition?(condition)
|
||||
key = condition['attribute_key']
|
||||
|
||||
conversation_filter = @conversation_filters[key]
|
||||
contact_filter = @contact_filters[key]
|
||||
message_filter = @message_filters[key]
|
||||
|
||||
if conversation_filter || contact_filter || message_filter
|
||||
operation_valid?(condition, conversation_filter || contact_filter || message_filter)
|
||||
else
|
||||
custom_attribute_present?(key, condition['custom_attribute_type'])
|
||||
end
|
||||
end
|
||||
|
||||
def operation_valid?(condition, filter)
|
||||
filter_operator = condition['filter_operator']
|
||||
|
||||
# attribute changed is a special case
|
||||
return true if filter_operator == 'attribute_changed'
|
||||
|
||||
filter['filter_operators'].include?(filter_operator)
|
||||
end
|
||||
|
||||
def custom_attribute_present?(attribute_key, attribute_model)
|
||||
attribute_model = attribute_model.presence || self.class::ATTRIBUTE_MODEL
|
||||
|
||||
@account.custom_attribute_definitions.where(
|
||||
attribute_model: attribute_model
|
||||
).find_by(attribute_key: attribute_key).present?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,209 @@
|
||||
require 'json'
|
||||
|
||||
class AutomationRules::ConditionsFilterService < FilterService
|
||||
ATTRIBUTE_MODEL = 'contact_attribute'.freeze
|
||||
|
||||
def initialize(rule, conversation = nil, options = {})
|
||||
super([], nil)
|
||||
# assign rule, conversation and account to instance variables
|
||||
@rule = rule
|
||||
@conversation = conversation
|
||||
@account = conversation.account
|
||||
|
||||
# setup filters from json file
|
||||
file = File.read('./lib/filters/filter_keys.yml')
|
||||
@filters = YAML.safe_load(file)
|
||||
|
||||
@conversation_filters = @filters['conversations']
|
||||
@contact_filters = @filters['contacts']
|
||||
@message_filters = @filters['messages']
|
||||
|
||||
@options = options
|
||||
@changed_attributes = options[:changed_attributes]
|
||||
end
|
||||
|
||||
def perform
|
||||
return false unless rule_valid?
|
||||
|
||||
@attribute_changed_query_filter = []
|
||||
|
||||
@rule.conditions.each_with_index do |query_hash, current_index|
|
||||
@attribute_changed_query_filter << query_hash and next if query_hash['filter_operator'] == 'attribute_changed'
|
||||
|
||||
apply_filter(query_hash, current_index)
|
||||
end
|
||||
|
||||
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
|
||||
records = perform_attribute_changed_filter(records) if @attribute_changed_query_filter.any?
|
||||
|
||||
records.any?
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error in AutomationRules::ConditionsFilterService: #{e.message}"
|
||||
Rails.logger.info "AutomationRules::ConditionsFilterService failed while processing rule #{@rule.id} for conversation #{@conversation.id}"
|
||||
false
|
||||
end
|
||||
|
||||
def rule_valid?
|
||||
is_valid = AutomationRules::ConditionValidationService.new(@rule).perform
|
||||
Rails.logger.info "Automation rule condition validation failed for rule id: #{@rule.id}" unless is_valid
|
||||
@rule.authorization_error! unless is_valid
|
||||
|
||||
is_valid
|
||||
end
|
||||
|
||||
def filter_operation(query_hash, current_index)
|
||||
if query_hash[:filter_operator] == 'starts_with'
|
||||
@filter_values["value_#{current_index}"] = "#{string_filter_values(query_hash)}%"
|
||||
like_filter_string(query_hash[:filter_operator], current_index)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def apply_filter(query_hash, current_index)
|
||||
conversation_filter = @conversation_filters[query_hash['attribute_key']]
|
||||
contact_filter = @contact_filters[query_hash['attribute_key']]
|
||||
message_filter = @message_filters[query_hash['attribute_key']]
|
||||
|
||||
if conversation_filter
|
||||
@query_string += conversation_query_string('conversations', conversation_filter, query_hash.with_indifferent_access, current_index)
|
||||
elsif contact_filter
|
||||
@query_string += contact_query_string(contact_filter, query_hash.with_indifferent_access, current_index)
|
||||
elsif message_filter
|
||||
@query_string += message_query_string(message_filter, query_hash.with_indifferent_access, current_index)
|
||||
elsif custom_attribute(query_hash['attribute_key'], @account, query_hash['custom_attribute_type'])
|
||||
# send table name according to attribute key right now we are supporting contact based custom attribute filter
|
||||
@query_string += custom_attribute_query(query_hash.with_indifferent_access, query_hash['custom_attribute_type'], current_index)
|
||||
end
|
||||
end
|
||||
|
||||
# If attribute_changed type filter is present perform this against array
|
||||
def perform_attribute_changed_filter(records)
|
||||
@attribute_changed_records = []
|
||||
current_attribute_changed_record = base_relation
|
||||
filter_based_on_attribute_change(records, current_attribute_changed_record)
|
||||
|
||||
@attribute_changed_records.uniq
|
||||
end
|
||||
|
||||
# Loop through attribute_changed_query_filter
|
||||
def filter_based_on_attribute_change(records, current_attribute_changed_record)
|
||||
@attribute_changed_query_filter.each do |filter|
|
||||
@changed_attributes = @changed_attributes.with_indifferent_access
|
||||
changed_attribute = @changed_attributes[filter['attribute_key']].presence
|
||||
|
||||
if changed_attribute[0].in?(filter['values']['from']) && changed_attribute[1].in?(filter['values']['to'])
|
||||
@attribute_changed_records = attribute_changed_filter_query(filter, records, current_attribute_changed_record)
|
||||
end
|
||||
current_attribute_changed_record = @attribute_changed_records
|
||||
end
|
||||
end
|
||||
|
||||
# We intersect with the record if query_operator-AND is present and union if query_operator-OR is present
|
||||
def attribute_changed_filter_query(filter, records, current_attribute_changed_record)
|
||||
if filter['query_operator'] == 'AND'
|
||||
@attribute_changed_records + (current_attribute_changed_record & records)
|
||||
else
|
||||
@attribute_changed_records + (current_attribute_changed_record | records)
|
||||
end
|
||||
end
|
||||
|
||||
def message_query_string(current_filter, query_hash, current_index)
|
||||
attribute_key = query_hash['attribute_key']
|
||||
query_operator = query_hash['query_operator']
|
||||
|
||||
attribute_key = 'processed_message_content' if attribute_key == 'content'
|
||||
|
||||
filter_operator_value = filter_operation(query_hash, current_index)
|
||||
|
||||
case current_filter['attribute_type']
|
||||
when 'standard'
|
||||
if current_filter['data_type'] == 'text'
|
||||
" LOWER(messages.#{attribute_key}) #{filter_operator_value} #{query_operator} "
|
||||
else
|
||||
" messages.#{attribute_key} #{filter_operator_value} #{query_operator} "
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This will be used in future for contact automation rule
|
||||
def contact_query_string(current_filter, query_hash, current_index)
|
||||
attribute_key = query_hash['attribute_key']
|
||||
query_operator = query_hash['query_operator']
|
||||
|
||||
filter_operator_value = filter_operation(query_hash, current_index)
|
||||
|
||||
case current_filter['attribute_type']
|
||||
when 'additional_attributes'
|
||||
" contacts.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
|
||||
when 'standard'
|
||||
" contacts.#{attribute_key} #{filter_operator_value} #{query_operator} "
|
||||
end
|
||||
end
|
||||
|
||||
def conversation_query_string(table_name, current_filter, query_hash, current_index)
|
||||
attribute_key = query_hash['attribute_key']
|
||||
query_operator = query_hash['query_operator']
|
||||
filter_operator_value = filter_operation(query_hash, current_index)
|
||||
|
||||
case current_filter['attribute_type']
|
||||
when 'additional_attributes'
|
||||
" #{table_name}.additional_attributes ->> '#{attribute_key}' #{filter_operator_value} #{query_operator} "
|
||||
when 'standard'
|
||||
if attribute_key == 'labels'
|
||||
build_label_query_string(query_hash, current_index, query_operator)
|
||||
else
|
||||
" #{table_name}.#{attribute_key} #{filter_operator_value} #{query_operator} "
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_label_query_string(query_hash, current_index, query_operator)
|
||||
case query_hash['filter_operator']
|
||||
when 'equal_to'
|
||||
return " 1=0 #{query_operator} " if query_hash['values'].blank?
|
||||
|
||||
value_placeholder = "value_#{current_index}"
|
||||
@filter_values[value_placeholder] = query_hash['values'].first
|
||||
" tags.name = :#{value_placeholder} #{query_operator} "
|
||||
when 'not_equal_to'
|
||||
return " 1=0 #{query_operator} " if query_hash['values'].blank?
|
||||
|
||||
value_placeholder = "value_#{current_index}"
|
||||
@filter_values[value_placeholder] = query_hash['values'].first
|
||||
" tags.name != :#{value_placeholder} #{query_operator} "
|
||||
when 'is_present'
|
||||
" tags.id IS NOT NULL #{query_operator} "
|
||||
when 'is_not_present'
|
||||
" tags.id IS NULL #{query_operator} "
|
||||
else
|
||||
" tags.id #{filter_operation(query_hash, current_index)} #{query_operator} "
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_relation
|
||||
records = Conversation.where(id: @conversation.id).joins(
|
||||
'LEFT OUTER JOIN contacts on conversations.contact_id = contacts.id'
|
||||
).joins(
|
||||
'LEFT OUTER JOIN messages on messages.conversation_id = conversations.id'
|
||||
)
|
||||
|
||||
# Only add label joins when label conditions exist
|
||||
if label_conditions?
|
||||
records = records.joins(
|
||||
'LEFT OUTER JOIN taggings ON taggings.taggable_id = conversations.id AND taggings.taggable_type = \'Conversation\''
|
||||
).joins(
|
||||
'LEFT OUTER JOIN tags ON taggings.tag_id = tags.id'
|
||||
)
|
||||
end
|
||||
|
||||
records = records.where(messages: { id: @options[:message].id }) if @options[:message].present?
|
||||
records
|
||||
end
|
||||
|
||||
def label_conditions?
|
||||
@rule.conditions.any? { |condition| condition['attribute_key'] == 'labels' }
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user