trabajo-afectivo/app/models/ticket.rb

1887 lines
64 KiB
Ruby
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class Ticket < ApplicationModel
include CanBeImported
include HasActivityStreamLog
include ChecksClientNotification
include ChecksLatestChangeObserved
include CanCsvImport
include ChecksHtmlSanitized
include HasHistory
include HasTags
include HasSearchIndexBackend
include HasOnlineNotifications
include HasKarmaActivityLog
include HasLinks
include HasObjectManagerAttributes
include HasTaskbars
include Ticket::CallsStatsTicketReopenLog
include Ticket::EnqueuesUserTicketCounterJob
include Ticket::ResetsPendingTimeSeconds
include Ticket::SetsCloseTime
include Ticket::SetsOnlineNotificationSeen
include Ticket::TouchesAssociations
include ::Ticket::Escalation
include ::Ticket::Subject
include ::Ticket::Assets
include ::Ticket::SearchIndex
include ::Ticket::Search
include ::Ticket::MergeHistory
store :preferences
before_create :check_generate, :check_defaults, :check_title, :set_default_state, :set_default_priority
before_update :check_defaults, :check_title, :reset_pending_time, :check_owner_active
# This must be loaded late as it depends on the internal before_create and before_update handlers of ticket.rb.
include Ticket::SetsLastOwnerUpdateTime
include HasTransactionDispatcher
# workflow checks should run after before_create and before_update callbacks
include ChecksCoreWorkflow
validates :group_id, presence: true
activity_stream_permission 'ticket.agent'
activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update
:create_article_type_id,
:create_article_sender_id,
:article_count,
:first_response_at,
:first_response_escalation_at,
:first_response_in_min,
:first_response_diff_in_min,
:close_at,
:close_escalation_at,
:close_in_min,
:close_diff_in_min,
:update_escalation_at,
:update_in_min,
:update_diff_in_min,
:last_contact_at,
:last_contact_agent_at,
:last_contact_customer_at,
:last_owner_update_at,
:preferences
history_attributes_ignored :create_article_type_id,
:create_article_sender_id,
:article_count,
:preferences
history_relation_object 'Ticket::Article', 'Mention'
sanitized_html :note
belongs_to :group, optional: true
belongs_to :organization, optional: true
has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
has_many :mentions, as: :mentionable, dependent: :destroy
belongs_to :state, class_name: 'Ticket::State', optional: true
belongs_to :priority, class_name: 'Ticket::Priority', optional: true
belongs_to :owner, class_name: 'User', optional: true
belongs_to :customer, class_name: 'User', optional: true
belongs_to :created_by, class_name: 'User', optional: true
belongs_to :updated_by, class_name: 'User', optional: true
belongs_to :create_article_type, class_name: 'Ticket::Article::Type', optional: true
belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender', optional: true
association_attributes_ignored :flags, :mentions
attr_accessor :callback_loop
=begin
processes tickets which have reached their pending time and sets next state_id
processed_tickets = Ticket.process_pending
returns
processed_tickets = [<Ticket>, ...]
=end
def self.process_pending
result = []
# process pending action tickets
pending_action = Ticket::StateType.find_by(name: 'pending action')
ticket_states_pending_action = Ticket::State.where(state_type_id: pending_action)
.where.not(next_state_id: nil)
if ticket_states_pending_action.present?
next_state_map = {}
ticket_states_pending_action.each do |state|
next_state_map[state.id] = state.next_state_id
end
tickets = where(state_id: next_state_map.keys)
.where('pending_time <= ?', Time.zone.now)
tickets.find_each(batch_size: 500) do |ticket|
Transaction.execute do
ticket.state_id = next_state_map[ticket.state_id]
ticket.updated_at = Time.zone.now
ticket.updated_by_id = 1
ticket.save!
end
result.push ticket
end
end
# process pending reminder tickets
pending_reminder = Ticket::StateType.find_by(name: 'pending reminder')
ticket_states_pending_reminder = Ticket::State.where(state_type_id: pending_reminder)
if ticket_states_pending_reminder.present?
reminder_state_map = {}
ticket_states_pending_reminder.each do |state|
reminder_state_map[state.id] = state.next_state_id
end
tickets = where(state_id: reminder_state_map.keys)
.where('pending_time <= ?', Time.zone.now)
tickets.find_each(batch_size: 500) do |ticket|
article_id = nil
article = Ticket::Article.last_customer_agent_article(ticket.id)
if article
article_id = article.id
end
# send notification
TransactionJob.perform_now(
object: 'Ticket',
type: 'reminder_reached',
object_id: ticket.id,
article_id: article_id,
user_id: 1,
)
result.push ticket
end
end
result
end
=begin
processes escalated tickets
processed_tickets = Ticket.process_escalation
returns
processed_tickets = [<Ticket>, ...]
=end
def self.process_escalation
result = []
# fetch all escalated and soon to be escalating tickets
where('escalation_at <= ?', Time.zone.now + 15.minutes).find_each(batch_size: 500) do |ticket|
article_id = nil
article = Ticket::Article.last_customer_agent_article(ticket.id)
if article
article_id = article.id
end
# send escalation
if ticket.escalation_at < Time.zone.now
TransactionJob.perform_now(
object: 'Ticket',
type: 'escalation',
object_id: ticket.id,
article_id: article_id,
user_id: 1,
)
result.push ticket
next
end
# check if warning need to be sent
TransactionJob.perform_now(
object: 'Ticket',
type: 'escalation_warning',
object_id: ticket.id,
article_id: article_id,
user_id: 1,
)
result.push ticket
end
result
end
=begin
processes tickets which auto unassign time has reached
processed_tickets = Ticket.process_auto_unassign
returns
processed_tickets = [<Ticket>, ...]
=end
def self.process_auto_unassign
# process pending action tickets
state_ids = Ticket::State.by_category(:work_on).pluck(:id)
return [] if state_ids.blank?
result = []
groups = Group.where(active: true).where('assignment_timeout IS NOT NULL AND groups.assignment_timeout != 0')
return [] if groups.blank?
groups.each do |group|
next if group.assignment_timeout.blank?
ticket_ids = Ticket.where('state_id IN (?) AND owner_id != 1 AND group_id = ? AND last_owner_update_at IS NOT NULL', state_ids, group.id).limit(600).pluck(:id)
ticket_ids.each do |ticket_id|
ticket = Ticket.find_by(id: ticket_id)
next if !ticket
minutes_since_last_assignment = Time.zone.now - ticket.last_owner_update_at
next if (minutes_since_last_assignment / 60) <= group.assignment_timeout
Transaction.execute do
ticket.owner_id = 1
ticket.updated_at = Time.zone.now
ticket.updated_by_id = 1
ticket.save!
end
result.push ticket
end
end
result
end
=begin
merge tickets
ticket = Ticket.find(123)
result = ticket.merge_to(
ticket_id: 123,
user_id: 123,
)
returns
result = true|false
=end
def merge_to(data)
# prevent cross merging tickets
target_ticket = Ticket.find_by(id: data[:ticket_id])
raise 'no target ticket given' if !target_ticket
raise Exceptions::UnprocessableEntity, 'ticket already merged, no merge into merged ticket possible' if target_ticket.state.state_type.name == 'merged'
# check different ticket ids
raise Exceptions::UnprocessableEntity, 'Can\'t merge ticket with it self!' if id == target_ticket.id
# update articles
Transaction.execute context: 'merge' do
Ticket::Article.where(ticket_id: id).each(&:touch)
# quiet update of reassign of articles
Ticket::Article.where(ticket_id: id).update_all(['ticket_id = ?', data[:ticket_id]]) # rubocop:disable Rails/SkipsModelValidations
# mark target ticket as updated
# otherwise the "received_merge" history entry
# will be the same as the last updated_at
# which might be a long time ago
target_ticket.updated_at = Time.zone.now
# add merge event to both ticket's history (Issue #2469 - Add information "Ticket merged" to History)
target_ticket.history_log(
'received_merge',
data[:user_id],
id_to: target_ticket.id,
id_from: id,
)
history_log(
'merged_into',
data[:user_id],
id_to: target_ticket.id,
id_from: id,
)
# create new merge article
Ticket::Article.create(
ticket_id: id,
type_id: Ticket::Article::Type.lookup(name: 'note').id,
sender_id: Ticket::Article::Sender.lookup(name: 'Agent').id,
body: 'merged',
internal: false,
created_by_id: data[:user_id],
updated_by_id: data[:user_id],
)
# search for mention duplicates and destroy them before moving mentions
Mention.duplicates(self, target_ticket).destroy_all
Mention.where(mentionable: self).update_all(mentionable_id: target_ticket.id) # rubocop:disable Rails/SkipsModelValidations
# reassign links to the new ticket
# rubocop:disable Rails/SkipsModelValidations
ticket_source_id = Link::Object.find_by(name: 'Ticket').id
# search for all duplicate source and target links and destroy them
# before link merging
Link.duplicates(
object1_id: ticket_source_id,
object1_value: id,
object2_value: data[:ticket_id]
).destroy_all
Link.where(
link_object_source_id: ticket_source_id,
link_object_source_value: id,
).update_all(link_object_source_value: data[:ticket_id])
Link.where(
link_object_target_id: ticket_source_id,
link_object_target_value: id,
).update_all(link_object_target_value: data[:ticket_id])
# rubocop:enable Rails/SkipsModelValidations
# link tickets
Link.add(
link_type: 'parent',
link_object_source: 'Ticket',
link_object_source_value: data[:ticket_id],
link_object_target: 'Ticket',
link_object_target_value: id
)
# external sync references
ExternalSync.migrate('Ticket', id, target_ticket.id)
# set state to 'merged'
self.state_id = Ticket::State.lookup(name: 'merged').id
# rest owner
self.owner_id = 1
# save ticket
save!
# touch new ticket (to broadcast change)
target_ticket.touch # rubocop:disable Rails/SkipsModelValidations
EventBuffer.add('transaction', {
object: target_ticket.class.name,
type: 'update.received_merge',
data: target_ticket,
changes: {},
id: target_ticket.id,
user_id: UserInfo.current_user_id,
created_at: Time.zone.now,
})
EventBuffer.add('transaction', {
object: self.class.name,
type: 'update.merged_into',
data: self,
changes: {},
id: id,
user_id: UserInfo.current_user_id,
created_at: Time.zone.now,
})
end
true
end
=begin
check if online notification should be shown in general as already seen with current state
ticket = Ticket.find(1)
seen = ticket.online_notification_seen_state(user_id_check)
returns
result = true # or false
=end
def online_notification_seen_state(user_id_check = nil)
state = Ticket::State.lookup(id: state_id)
state_type = Ticket::StateType.lookup(id: state.state_type_id)
# always to set unseen for ticket owner and users which did not the update
return false if state_type.name != 'merged' && user_id_check && user_id_check == owner_id && user_id_check != updated_by_id
# set all to seen if pending action state is a closed or merged state
if state_type.name == 'pending action' && state.next_state_id
state = Ticket::State.lookup(id: state.next_state_id)
state_type = Ticket::StateType.lookup(id: state.state_type_id)
end
# set all to seen if new state is pending reminder state
if state_type.name == 'pending reminder'
if user_id_check
return false if owner_id == 1
return false if updated_by_id != owner_id && user_id_check == owner_id
return true
end
return true
end
# set all to seen if new state is a closed or merged state
return true if state_type.name == 'closed'
return true if state_type.name == 'merged'
false
end
=begin
get count of tickets and tickets which match on selector
@param [Hash] selectors hash with conditions
@oparam [Hash] options
@option options [String] :access can be 'full', 'read', 'create' or 'ignore' (ignore means a selector over all tickets), defaults to 'full'
@option options [Integer] :limit of tickets to return
@option options [User] :user is a current user
@option options [Integer] :execution_time is a current user
@return [Integer, [<Ticket>]]
@example
ticket_count, tickets = Ticket.selectors(params[:condition], limit: limit, current_user: current_user, access: 'full')
ticket_count # count of found tickets
tickets # tickets
=end
def self.selectors(selectors, options)
limit = options[:limit] || 10
current_user = options[:current_user]
access = options[:access] || 'full'
raise 'no selectors given' if !selectors
query, bind_params, tables = selector2sql(selectors, current_user: current_user, execution_time: options[:execution_time])
return [] if !query
ActiveRecord::Base.transaction(requires_new: true) do
if !current_user || access == 'ignore'
ticket_count = Ticket.distinct.where(query, *bind_params).joins(tables).count
tickets = Ticket.distinct.where(query, *bind_params).joins(tables).limit(limit)
return [ticket_count, tickets]
end
tickets = "TicketPolicy::#{access.camelize}Scope".constantize
.new(current_user).resolve
.distinct
.where(query, *bind_params)
.joins(tables)
return [tickets.count, tickets.limit(limit)]
rescue ActiveRecord::StatementInvalid => e
Rails.logger.error e
raise ActiveRecord::Rollback
end
[]
end
=begin
generate condition query to search for tickets based on condition
query_condition, bind_condition, tables = selector2sql(params[:condition], current_user: current_user)
condition example
{
'ticket.title' => {
operator: 'contains', # contains not
value: 'some value',
},
'ticket.state_id' => {
operator: 'is',
value: [1,2,5]
},
'ticket.created_at' => {
operator: 'after (absolute)', # after,before
value: '2015-10-17T06:00:00.000Z',
},
'ticket.created_at' => {
operator: 'within next (relative)', # within next, within last, after, before
range: 'day', # minute|hour|day|month|year
value: '25',
},
'ticket.owner_id' => {
operator: 'is', # is not
pre_condition: 'current_user.id',
},
'ticket.owner_id' => {
operator: 'is', # is not
pre_condition: 'specific',
value: 4711,
},
'ticket.escalation_at' => {
operator: 'is not', # not
value: nil,
},
'ticket.tags' => {
operator: 'contains all', # contains all|contains one|contains all not|contains one not
value: 'tag1, tag2',
},
}
=end
def self.selector2sql(selectors, options = {})
current_user = options[:current_user]
current_user_id = UserInfo.current_user_id
if current_user
current_user_id = current_user.id
end
return if !selectors
# remember query and bind params
query = ''
bind_params = []
like = Rails.application.config.db_like
if selectors.respond_to?(:permit!)
selectors = selectors.permit!.to_h
end
# get tables to join
tables = ''
selectors.each do |attribute, selector_raw|
attributes = attribute.split('.')
selector = selector_raw.stringify_keys
next if !attributes[1]
next if attributes[0] == 'execution_time'
next if tables.include?(attributes[0])
next if attributes[0] == 'ticket' && attributes[1] != 'mention_user_ids'
next if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids' && selector['pre_condition'] == 'not_set'
if query != ''
query += ' AND '
end
case attributes[0]
when 'customer'
tables += ', users customers'
query += 'tickets.customer_id = customers.id'
when 'organization'
tables += ', organizations'
query += 'tickets.organization_id = organizations.id'
when 'owner'
tables += ', users owners'
query += 'tickets.owner_id = owners.id'
when 'article'
tables += ', ticket_articles articles'
query += 'tickets.id = articles.ticket_id'
when 'ticket_state'
tables += ', ticket_states'
query += 'tickets.state_id = ticket_states.id'
when 'ticket'
if attributes[1] == 'mention_user_ids'
tables += ', mentions'
query += "tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket'"
end
else
raise "invalid selector #{attribute.inspect}->#{attributes.inspect}"
end
end
# add conditions
no_result = false
selectors.each do |attribute, selector_raw|
# validation
raise "Invalid selector #{selector_raw.inspect}" if !selector_raw
raise "Invalid selector #{selector_raw.inspect}" if !selector_raw.respond_to?(:key?)
selector = selector_raw.stringify_keys
raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator']
raise "Invalid selector, operator #{selector['operator']} is invalid #{selector.inspect}" if !selector['operator'].match?(%r{^(is|is\snot|contains|contains\s(not|all|one|all\snot|one\snot)|(after|before)\s\(absolute\)|(within\snext|within\slast|after|before|till|from)\s\(relative\))|(is\sin\sworking\stime|is\snot\sin\sworking\stime)$})
# validate value / allow blank but only if pre_condition exists and is not specific
if !selector.key?('value') ||
(selector['value'].instance_of?(Array) && selector['value'].respond_to?(:blank?) && selector['value'].blank?) ||
(selector['operator'].start_with?('contains') && selector['value'].respond_to?(:blank?) && selector['value'].blank?)
return nil if selector['pre_condition'].nil?
return nil if selector['pre_condition'].respond_to?(:blank?) && selector['pre_condition'].blank?
return nil if selector['pre_condition'] == 'specific'
end
# validate pre_condition values
return nil if selector['pre_condition'] && selector['pre_condition'] !~ %r{^(not_set|current_user\.|specific)}
# get attributes
attributes = attribute.split('.')
attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name(attributes[1])}"
# magic selectors
if attributes[0] == 'ticket' && attributes[1] == 'out_of_office_replacement_id'
attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name('owner_id')}"
end
if attributes[0] == 'ticket' && attributes[1] == 'tags'
selector['value'] = selector['value'].split(',').collect(&:strip)
end
if selector['operator'].include?('in working time')
next if attributes[1] != 'calendar_id'
raise 'Please enable execution_time feature to use it (currently only allowed for triggers and schedulers)' if !options[:execution_time]
biz = Calendar.lookup(id: selector['value'])&.biz
next if biz.blank?
if (selector['operator'] == 'is in working time' && !biz.in_hours?(Time.zone.now)) || (selector['operator'] == 'is not in working time' && biz.in_hours?(Time.zone.now))
no_result = true
break
end
# skip to next condition
next
end
if query != ''
query += ' AND '
end
# because of no grouping support we select not_set by sub select for mentions
if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids'
if selector['pre_condition'] == 'not_set'
query += if selector['operator'] == 'is'
"(SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id) IS NULL"
else
"1 = (SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id)"
end
else
query += if selector['operator'] == 'is'
'mentions.user_id IN (?)'
else
'mentions.user_id NOT IN (?)'
end
if selector['pre_condition'] == 'current_user.id'
bind_params.push current_user_id
else
bind_params.push selector['value']
end
end
next
end
if selector['operator'] == 'is'
if selector['pre_condition'] == 'not_set'
if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
query += "(#{attribute} IS NULL OR #{attribute} IN (?))"
bind_params.push 1
else
query += "#{attribute} IS NULL"
end
elsif selector['pre_condition'] == 'current_user.id'
raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
query += "#{attribute} IN (?)"
if attributes[1] == 'out_of_office_replacement_id'
bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
else
bind_params.push current_user_id
end
elsif selector['pre_condition'] == 'current_user.organization_id'
raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
query += "#{attribute} IN (?)"
user = User.find_by(id: current_user_id)
bind_params.push user.organization_id
else
# rubocop:disable Style/IfInsideElse
if selector['value'].nil?
query += "#{attribute} IS NULL"
else
if attributes[1] == 'out_of_office_replacement_id'
query += "#{attribute} IN (?)"
bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
else
if selector['value'].class != Array
selector['value'] = [selector['value']]
end
query += if selector['value'].include?('')
"(#{attribute} IN (?) OR #{attribute} IS NULL)"
else
"#{attribute} IN (?)"
end
bind_params.push selector['value']
end
end
# rubocop:enable Style/IfInsideElse
end
elsif selector['operator'] == 'is not'
if selector['pre_condition'] == 'not_set'
if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
query += "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
bind_params.push 1
else
query += "#{attribute} IS NOT NULL"
end
elsif selector['pre_condition'] == 'current_user.id'
query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
if attributes[1] == 'out_of_office_replacement_id'
bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id)
else
bind_params.push current_user_id
end
elsif selector['pre_condition'] == 'current_user.organization_id'
query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
user = User.find_by(id: current_user_id)
bind_params.push user.organization_id
else
# rubocop:disable Style/IfInsideElse
if selector['value'].nil?
query += "#{attribute} IS NOT NULL"
else
if attributes[1] == 'out_of_office_replacement_id'
bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id)
query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
else
if selector['value'].class != Array
selector['value'] = [selector['value']]
end
query += if selector['value'].include?('')
"(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))"
else
"(#{attribute} IS NULL OR #{attribute} NOT IN (?))"
end
bind_params.push selector['value']
end
end
# rubocop:enable Style/IfInsideElse
end
elsif selector['operator'] == 'contains'
query += "#{attribute} #{like} (?)"
value = "%#{selector['value']}%"
bind_params.push value
elsif selector['operator'] == 'contains not'
query += "#{attribute} NOT #{like} (?)"
value = "%#{selector['value']}%"
bind_params.push value
elsif selector['operator'] == 'contains all' && attributes[0] == 'ticket' && attributes[1] == 'tags'
query += "? = (
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
tags
WHERE
tickets.id = tags.o_id AND
tag_objects.id = tags.tag_object_id AND
tag_objects.name = 'Ticket' AND
tag_items.id = tags.tag_item_id AND
tag_items.name IN (?)
)"
bind_params.push selector['value'].count
bind_params.push selector['value']
elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket' && attributes[1] == 'tags'
tables += ', tag_objects, tag_items, tags'
query += "
tickets.id = tags.o_id AND
tag_objects.id = tags.tag_object_id AND
tag_objects.name = 'Ticket' AND
tag_items.id = tags.tag_item_id AND
tag_items.name IN (?)"
bind_params.push selector['value']
elsif selector['operator'] == 'contains all not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
query += "0 = (
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
tags
WHERE
tickets.id = tags.o_id AND
tag_objects.id = tags.tag_object_id AND
tag_objects.name = 'Ticket' AND
tag_items.id = tags.tag_item_id AND
tag_items.name IN (?)
)"
bind_params.push selector['value']
elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
query += "(
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
tags
WHERE
tickets.id = tags.o_id AND
tag_objects.id = tags.tag_object_id AND
tag_objects.name = 'Ticket' AND
tag_items.id = tags.tag_item_id AND
tag_items.name IN (?)
) BETWEEN 0 AND 0"
bind_params.push selector['value']
elsif selector['operator'] == 'before (absolute)'
query += "#{attribute} <= ?"
bind_params.push selector['value']
elsif selector['operator'] == 'after (absolute)'
query += "#{attribute} >= ?"
bind_params.push selector['value']
elsif selector['operator'] == 'within last (relative)'
query += "#{attribute} BETWEEN ? AND ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.ago
when 'hour'
time = selector['value'].to_i.hours.ago
when 'day'
time = selector['value'].to_i.days.ago
when 'month'
time = selector['value'].to_i.months.ago
when 'year'
time = selector['value'].to_i.years.ago
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
bind_params.push Time.zone.now
elsif selector['operator'] == 'within next (relative)'
query += "#{attribute} BETWEEN ? AND ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.from_now
when 'hour'
time = selector['value'].to_i.hours.from_now
when 'day'
time = selector['value'].to_i.days.from_now
when 'month'
time = selector['value'].to_i.months.from_now
when 'year'
time = selector['value'].to_i.years.from_now
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push Time.zone.now
bind_params.push time
elsif selector['operator'] == 'before (relative)'
query += "#{attribute} <= ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.ago
when 'hour'
time = selector['value'].to_i.hours.ago
when 'day'
time = selector['value'].to_i.days.ago
when 'month'
time = selector['value'].to_i.months.ago
when 'year'
time = selector['value'].to_i.years.ago
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
elsif selector['operator'] == 'after (relative)'
query += "#{attribute} >= ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.from_now
when 'hour'
time = selector['value'].to_i.hours.from_now
when 'day'
time = selector['value'].to_i.days.from_now
when 'month'
time = selector['value'].to_i.months.from_now
when 'year'
time = selector['value'].to_i.years.from_now
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
elsif selector['operator'] == 'till (relative)'
query += "#{attribute} <= ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.from_now
when 'hour'
time = selector['value'].to_i.hours.from_now
when 'day'
time = selector['value'].to_i.days.from_now
when 'month'
time = selector['value'].to_i.months.from_now
when 'year'
time = selector['value'].to_i.years.from_now
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
elsif selector['operator'] == 'from (relative)'
query += "#{attribute} >= ?"
time = nil
case selector['range']
when 'minute'
time = selector['value'].to_i.minutes.ago
when 'hour'
time = selector['value'].to_i.hours.ago
when 'day'
time = selector['value'].to_i.days.ago
when 'month'
time = selector['value'].to_i.months.ago
when 'year'
time = selector['value'].to_i.years.ago
else
raise "Unknown selector attributes '#{selector.inspect}'"
end
bind_params.push time
else
raise "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'"
end
end
return if no_result
[query, bind_params, tables]
end
=begin
perform changes on ticket
ticket.perform_changes(trigger, 'trigger', item, current_user_id)
# or
ticket.perform_changes(job, 'job', item, current_user_id)
=end
def perform_changes(performable, perform_origin, item = nil, current_user_id = nil)
perform = performable.perform
logger.debug { "Perform #{perform_origin} #{perform.inspect} on Ticket.find(#{id})" }
article = begin
Ticket::Article.find_by(id: item.try(:dig, :article_id))
rescue ArgumentError
nil
end
# if the configuration contains the deletion of the ticket then
# we skip all other ticket changes because they does not matter
if perform['ticket.action'].present? && perform['ticket.action']['value'] == 'delete'
perform.each_key do |key|
(object_name, attribute) = key.split('.', 2)
next if object_name != 'ticket'
next if attribute == 'action'
perform.delete(key)
end
end
perform_notification = {}
perform_article = {}
changed = false
perform.each do |key, value|
(object_name, attribute) = key.split('.', 2)
raise "Unable to update object #{object_name}.#{attribute}, only can update tickets, send notifications and create articles!" if object_name != 'ticket' && object_name != 'article' && object_name != 'notification'
# send notification/create article (after changes are done)
if object_name == 'article'
perform_article[key] = value
next
end
if object_name == 'notification'
perform_notification[key] = value
next
end
# Apply pending_time changes
if key == 'ticket.pending_time'
new_value = case value['operator']
when 'static'
value['value']
when 'relative'
pendtil = Time.zone.now
val = value['value'].to_i
case value['range']
when 'day'
pendtil += val.days
when 'minute'
pendtil += val.minutes
when 'hour'
pendtil += val.hours
when 'month'
pendtil += val.months
when 'year'
pendtil += val.years
end
pendtil
end
if new_value
self[attribute] = new_value
changed = true
next
end
end
# update tags
if key == 'ticket.tags'
next if value['value'].blank?
tags = value['value'].split(',')
case value['operator']
when 'add'
tags.each do |tag|
tag_add(tag, current_user_id || 1)
end
when 'remove'
tags.each do |tag|
tag_remove(tag, current_user_id || 1)
end
else
logger.error "Unknown #{attribute} operator #{value['operator']}"
end
next
end
# delete ticket
if key == 'ticket.action'
next if value['value'].blank?
next if value['value'] != 'delete'
logger.info { "Deleted ticket from #{perform_origin} #{perform.inspect} Ticket.find(#{id})" }
destroy!
next
end
# lookup pre_condition
if value['pre_condition']
if value['pre_condition'].start_with?('not_set')
value['value'] = 1
elsif value['pre_condition'].start_with?('current_user.')
raise 'Unable to use current_user, got no current_user_id for ticket.perform_changes' if !current_user_id
value['value'] = current_user_id
end
end
# update ticket
next if self[attribute].to_s == value['value'].to_s
changed = true
self[attribute] = value['value']
logger.debug { "set #{object_name}.#{attribute} = #{value['value'].inspect} for ticket_id #{id}" }
end
if changed
save!
end
objects = build_notification_template_objects(article)
perform_article.each do |key, value|
raise 'Unable to create article, we only support article.note' if key != 'article.note'
add_trigger_note(id, value, objects, perform_origin)
end
perform_notification.each do |key, value|
# send notification
case key
when 'notification.sms'
send_sms_notification(value, article, perform_origin)
next
when 'notification.email'
send_email_notification(value, article, perform_origin)
when 'notification.webhook'
TriggerWebhookJob.perform_later(performable, self, article)
end
end
true
end
=begin
perform changes on ticket
ticket.add_trigger_note(ticket_id, note, objects, perform_origin)
=end
def add_trigger_note(ticket_id, note, objects, perform_origin)
rendered_subject = NotificationFactory::Mailer.template(
templateInline: note[:subject],
objects: objects,
quote: true,
)
rendered_body = NotificationFactory::Mailer.template(
templateInline: note[:body],
objects: objects,
quote: true,
)
Ticket::Article.create!(
ticket_id: ticket_id,
subject: rendered_subject,
content_type: 'text/html',
body: rendered_body,
internal: note[:internal],
sender: Ticket::Article::Sender.find_by(name: 'System'),
type: Ticket::Article::Type.find_by(name: 'note'),
preferences: {
perform_origin: perform_origin,
notification: true,
},
updated_by_id: 1,
created_by_id: 1,
)
end
=begin
perform active triggers on ticket
Ticket.perform_triggers(ticket, article, item, options)
=end
def self.perform_triggers(ticket, article, item, options = {})
recursive = Setting.get('ticket_trigger_recursive')
type = options[:type] || item[:type]
local_options = options.clone
local_options[:type] = type
local_options[:reset_user_id] = true
local_options[:disable] = ['Transaction::Notification']
local_options[:trigger_ids] ||= {}
local_options[:trigger_ids][ticket.id.to_s] ||= []
local_options[:loop_count] ||= 0
local_options[:loop_count] += 1
ticket_trigger_recursive_max_loop = Setting.get('ticket_trigger_recursive_max_loop')&.to_i || 10
if local_options[:loop_count] > ticket_trigger_recursive_max_loop
message = "Stopped perform_triggers for this object (Ticket/#{ticket.id}), because loop count was #{local_options[:loop_count]}!"
logger.info { message }
return [false, message]
end
triggers = if Rails.configuration.db_case_sensitive
::Trigger.where(active: true).order(Arel.sql('LOWER(name)'))
else
::Trigger.where(active: true).order(:name)
end
return [true, 'No triggers active'] if triggers.blank?
# check if notification should be send because of customer emails
send_notification = true
if local_options[:send_notification] == false
send_notification = false
elsif item[:article_id]
article = Ticket::Article.lookup(id: item[:article_id])
if article&.preferences && article.preferences['send-auto-response'] == false
send_notification = false
end
end
Transaction.execute(local_options) do
triggers.each do |trigger|
logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
condition = trigger.condition
# check if one article attribute is used
one_has_changed_done = false
article_selector = false
trigger.condition.each_key do |key|
(object_name, attribute) = key.split('.', 2)
next if object_name != 'article'
next if attribute == 'id'
article_selector = true
end
if article && article_selector
one_has_changed_done = true
end
if article && type == 'update'
one_has_changed_done = true
end
# check ticket "has changed" options
has_changed_done = true
condition.each do |key, value|
next if value.blank?
next if value['operator'].blank?
next if !value['operator']['has changed']
# remove condition item, because it has changed
(object_name, attribute) = key.split('.', 2)
next if object_name != 'ticket'
next if item[:changes].blank?
next if !item[:changes].key?(attribute)
condition.delete(key)
one_has_changed_done = true
end
# check if we have not matching "has changed" attributes
condition.each_value do |value|
next if value.blank?
next if value['operator'].blank?
next if !value['operator']['has changed']
has_changed_done = false
break
end
# check ticket action
if condition['ticket.action']
next if condition['ticket.action']['operator'] == 'is' && condition['ticket.action']['value'] != type
next if condition['ticket.action']['operator'] != 'is' && condition['ticket.action']['value'] == type
condition.delete('ticket.action')
end
next if !has_changed_done
# check in min one attribute of condition has changed on update
one_has_changed_condition = false
if type == 'update'
# verify if ticket condition exists
condition.each_key do |key|
(object_name, attribute) = key.split('.', 2)
next if object_name != 'ticket'
one_has_changed_condition = true
next if item[:changes].blank?
next if !item[:changes].key?(attribute)
one_has_changed_done = true
break
end
next if one_has_changed_condition && !one_has_changed_done
end
# check if ticket selector is matching
condition['ticket.id'] = {
operator: 'is',
value: ticket.id,
}
next if article_selector && !article
# check if article selector is matching
if article_selector
condition['article.id'] = {
operator: 'is',
value: article.id,
}
end
user_id = ticket.updated_by_id
if article
user_id = article.updated_by_id
end
user = User.lookup(id: user_id)
# verify is condition is matching
ticket_count, tickets = Ticket.selectors(condition, limit: 1, execution_time: true, current_user: user, access: 'ignore')
next if ticket_count.blank?
next if ticket_count.zero?
next if tickets.first.id != ticket.id
if recursive == false && local_options[:loop_count] > 1
message = "Do not execute recursive triggers per default until Zammad 3.0. With Zammad 3.0 and higher the following trigger is executed '#{trigger.name}' on Ticket:#{ticket.id}. Please review your current triggers and change them if needed."
logger.info { message }
return [true, message]
end
if article && send_notification == false && trigger.perform['notification.email'] && trigger.perform['notification.email']['recipient']
recipient = trigger.perform['notification.email']['recipient']
local_options[:send_notification] = false
if recipient.include?('ticket_customer') || recipient.include?('article_last_sender')
logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because sender do not want to get auto responder for object (Ticket/#{ticket.id}/Article/#{article.id})" }
next
end
end
if local_options[:trigger_ids][ticket.id.to_s].include?(trigger.id)
logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because was already executed for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
next
end
local_options[:trigger_ids][ticket.id.to_s].push trigger.id
logger.info { "Execute trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
ticket.perform_changes(trigger, 'trigger', item, user_id)
if recursive == true
TransactionDispatcher.commit(local_options)
end
end
end
[true, ticket, local_options]
end
=begin
get all email references headers of a ticket, to exclude some, parse it as array into method
references = ticket.get_references
result
['message-id-1234', 'message-id-5678']
ignore references header(s)
references = ticket.get_references(['message-id-5678'])
result
['message-id-1234']
=end
def get_references(ignore = [])
references = []
Ticket::Article.select('in_reply_to, message_id').where(ticket_id: id).each do |article|
if article.in_reply_to.present?
references.push article.in_reply_to
end
next if article.message_id.blank?
references.push article.message_id
end
ignore.each do |item|
references.delete(item)
end
references
end
=begin
get all articles of a ticket in correct order (overwrite active record default method)
articles = ticket.articles
result
[article1, article2]
=end
def articles
Ticket::Article.where(ticket_id: id).order(:created_at, :id)
end
# Get whichever #last_contact_* was later
# This is not identical to #last_contact_at
# It returns time to last original (versus follow up) contact
# @return [Time, nil]
def last_original_update_at
[last_contact_agent_at, last_contact_customer_at].compact.max
end
# true if conversation did happen and agent responded
# false if customer is waiting for response or agent reached out and customer did not respond yet
# @return [Bool]
def agent_responded?
return false if last_contact_customer_at.blank?
return false if last_contact_agent_at.blank?
last_contact_customer_at < last_contact_agent_at
end
private
def check_generate
return true if number
self.number = Ticket::Number.generate
true
end
def check_title
return true if !title
title.gsub!(%r{\s|\t|\r}, ' ')
true
end
def check_defaults
if !owner_id
self.owner_id = 1
end
return true if !customer_id
customer = User.find_by(id: customer_id)
return true if !customer
return true if organization_id == customer.organization_id
self.organization_id = customer.organization_id
true
end
def reset_pending_time
# ignore if no state has changed
return true if !changes_to_save['state_id']
# ignore if new state is blank and
# let handle ActiveRecord the error
return if state_id.blank?
# check if new state isn't pending*
current_state = Ticket::State.lookup(id: state_id)
current_state_type = Ticket::StateType.lookup(id: current_state.state_type_id)
# in case, set pending_time to nil
return true if current_state_type.name.match?(%r{^pending}i)
self.pending_time = nil
true
end
def set_default_state
return true if state_id
default_ticket_state = Ticket::State.find_by(default_create: true)
return true if !default_ticket_state
self.state_id = default_ticket_state.id
true
end
def set_default_priority
return true if priority_id
default_ticket_priority = Ticket::Priority.find_by(default_create: true)
return true if !default_ticket_priority
self.priority_id = default_ticket_priority.id
true
end
def check_owner_active
return true if Setting.get('import_mode')
# only change the owner for non closed Tickets for historical/reporting reasons
return true if state.present? && Ticket::StateType.lookup(id: state.state_type_id)&.name == 'closed'
# return when ticket is unassigned
return true if owner_id.blank?
return true if owner_id == 1
# return if owner is active, is agent and has access to group of ticket
return true if owner.active? && owner.permissions?('ticket.agent') && owner.group_access?(group_id, 'full')
# else set the owner of the ticket to the default user as unassigned
self.owner_id = 1
true
end
# articles.last breaks (returns the wrong article)
# if another email notification trigger preceded this one
# (see https://github.com/zammad/zammad/issues/1543)
def build_notification_template_objects(article)
{
ticket: self,
article: article || articles.last
}
end
def send_email_notification(value, article, perform_origin)
# value['recipient'] was a string in the past (single-select) so we convert it to array if needed
value_recipient = Array(value['recipient'])
recipients_raw = []
value_recipient.each do |recipient|
case recipient
when 'article_last_sender'
if article.present?
if article.reply_to.present?
recipients_raw.push(article.reply_to)
elsif article.from.present?
recipients_raw.push(article.from)
elsif article.origin_by_id
email = User.find_by(id: article.origin_by_id).email
recipients_raw.push(email)
elsif article.created_by_id
email = User.find_by(id: article.created_by_id).email
recipients_raw.push(email)
end
end
when 'ticket_customer'
email = User.find_by(id: customer_id).email
recipients_raw.push(email)
when 'ticket_owner'
email = User.find_by(id: owner_id).email
recipients_raw.push(email)
when 'ticket_agents'
User.group_access(group_id, 'full').sort_by(&:login).each do |user|
recipients_raw.push(user.email)
end
when %r{\Auserid_(\d+)\z}
user = User.lookup(id: $1)
if !user
logger.warn "Can't find configured Trigger Email recipient User with ID '#{$1}'"
next
end
recipients_raw.push(user.email)
else
logger.error "Unknown email notification recipient '#{recipient}'"
next
end
end
recipients_checked = []
recipients_raw.each do |recipient_email|
users = User.where(email: recipient_email)
next if users.any? { |user| !trigger_based_notification?(user) }
# send notifications only to email addresses
next if recipient_email.blank?
# check if address is valid
begin
Mail::AddressList.new(recipient_email).addresses.each do |address|
recipient_email = address.address
email_address_validation = EmailAddressValidation.new(recipient_email)
break if recipient_email.present? && email_address_validation.valid_format?
end
rescue
if recipient_email.present?
if recipient_email !~ %r{^(.+?)<(.+?)@(.+?)>$}
next # no usable format found
end
recipient_email = "#{$2}@#{$3}" # rubocop:disable Lint/OutOfRangeRegexpRef
end
end
email_address_validation = EmailAddressValidation.new(recipient_email)
next if !email_address_validation.valid_format?
# do not send notification if system address
next if EmailAddress.exists?(email: recipient_email.downcase)
# do not sent notifications to this recipients
send_no_auto_response_reg_exp = Setting.get('send_no_auto_response_reg_exp')
begin
next if recipient_email.match?(%r{#{send_no_auto_response_reg_exp}}i)
rescue => e
logger.error "Invalid regex '#{send_no_auto_response_reg_exp}' in setting send_no_auto_response_reg_exp"
logger.error e
next if recipient_email.match?(%r{(mailer-daemon|postmaster|abuse|root|noreply|noreply.+?|no-reply|no-reply.+?)@.+?}i)
end
# check if notification should be send because of customer emails
if article.present? && article.preferences.fetch('is-auto-response', false) == true && article.from && article.from =~ %r{#{Regexp.quote(recipient_email)}}i
logger.info "Send no trigger based notification to #{recipient_email} because of auto response tagged incoming email"
next
end
# loop protection / check if maximal count of trigger mail has reached
map = {
10 => 10,
30 => 15,
60 => 25,
180 => 50,
600 => 100,
}
skip = false
map.each do |minutes, count|
already_sent = Ticket::Article.where(
ticket_id: id,
sender: Ticket::Article::Sender.find_by(name: 'System'),
type: Ticket::Article::Type.find_by(name: 'email'),
).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
next if already_sent < count
logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} for this ticket within last #{minutes} minutes (loop protection)"
skip = true
break
end
next if skip
map = {
10 => 30,
30 => 60,
60 => 120,
180 => 240,
600 => 360,
}
skip = false
map.each do |minutes, count|
already_sent = Ticket::Article.where(
sender: Ticket::Article::Sender.find_by(name: 'System'),
type: Ticket::Article::Type.find_by(name: 'email'),
).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
next if already_sent < count
logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{minutes} minutes (loop protection)"
skip = true
break
end
next if skip
email = recipient_email.downcase.strip
next if recipients_checked.include?(email)
recipients_checked.push(email)
end
return if recipients_checked.blank?
recipient_string = recipients_checked.join(', ')
group_id = self.group_id
return if !group_id
email_address = Group.find(group_id).email_address
if !email_address
logger.info "Unable to send trigger based notification to #{recipient_string} because no email address is set for group '#{group.name}'"
return
end
if !email_address.channel_id
logger.info "Unable to send trigger based notification to #{recipient_string} because no channel is set for email address '#{email_address.email}' (id: #{email_address.id})"
return
end
security = nil
if Setting.get('smime_integration')
sign = value['sign'].present? && value['sign'] != 'no'
encryption = value['encryption'].present? && value['encryption'] != 'no'
security = {
type: 'S/MIME',
sign: {
success: false,
},
encryption: {
success: false,
},
}
if sign
sign_found = false
begin
list = Mail::AddressList.new(email_address.email)
from = list.addresses.first.to_s
cert = SMIMECertificate.for_sender_email_address(from)
if cert && !cert.expired?
sign_found = true
security[:sign][:success] = true
security[:sign][:comment] = "certificate for #{email_address.email} found"
end
rescue # rubocop:disable Lint/SuppressedException
end
if value['sign'] == 'discard' && !sign_found
logger.info "Unable to send trigger based notification to #{recipient_string} because of missing group #{group.name} email #{email_address.email} certificate for signing (discarding notification)."
return
end
end
if encryption
certs_found = false
begin
SMIMECertificate.for_recipipent_email_addresses!(recipients_checked)
certs_found = true
security[:encryption][:success] = true
security[:encryption][:comment] = "certificates found for #{recipient_string}"
rescue # rubocop:disable Lint/SuppressedException
end
if value['encryption'] == 'discard' && !certs_found
logger.info "Unable to send trigger based notification to #{recipient_string} because public certificate is not available for encryption (discarding notification)."
return
end
end
end
objects = build_notification_template_objects(article)
# get subject
subject = NotificationFactory::Mailer.template(
templateInline: value['subject'],
objects: objects,
quote: false,
)
subject = subject_build(subject)
body = NotificationFactory::Mailer.template(
templateInline: value['body'],
objects: objects,
quote: true,
)
(body, attachments_inline) = HtmlSanitizer.replace_inline_images(body, id)
preferences = {}
preferences[:perform_origin] = perform_origin
if security.present?
preferences[:security] = security
end
message = Ticket::Article.create(
ticket_id: id,
to: recipient_string,
subject: subject,
content_type: 'text/html',
body: body,
internal: value['internal'] || false, # default to public if value was not set
sender: Ticket::Article::Sender.find_by(name: 'System'),
type: Ticket::Article::Type.find_by(name: 'email'),
preferences: preferences,
updated_by_id: 1,
created_by_id: 1,
)
attachments_inline.each do |attachment|
Store.add(
object: 'Ticket::Article',
o_id: message.id,
data: attachment[:data],
filename: attachment[:filename],
preferences: attachment[:preferences],
)
end
original_article = objects[:article]
if ActiveModel::Type::Boolean.new.cast(value['include_attachments']) == true && original_article&.attachments.present?
original_article.clone_attachments('Ticket::Article', message.id, only_attached_attachments: true)
end
if original_article&.should_clone_inline_attachments? # rubocop:disable Style/GuardClause
original_article.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true)
original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning
end
end
def sms_recipients_by_type(recipient_type, article)
case recipient_type
when 'article_last_sender'
return nil if article.blank?
if article.origin_by_id
article.origin_by_id
elsif article.created_by_id
article.created_by_id
end
when 'ticket_customer'
customer_id
when 'ticket_owner'
owner_id
when 'ticket_agents'
User.group_access(group_id, 'full').sort_by(&:login)
when %r{\Auserid_(\d+)\z}
return $1 if User.exists?($1)
logger.warn "Can't find configured Trigger SMS recipient User with ID '#{$1}'"
nil
else
logger.error "Unknown sms notification recipient '#{recipient}'"
nil
end
end
def build_sms_recipients_list(value, article)
Array(value['recipient'])
.each_with_object([]) { |recipient_type, sum| sum.concat(Array(sms_recipients_by_type(recipient_type, article))) }
.map { |user_or_id| user_or_id.is_a?(User) ? user_or_id : User.lookup(id: user_or_id) }
.uniq(&:id)
.select { |user| user.mobile.present? }
end
def send_sms_notification(value, article, perform_origin)
sms_recipients = build_sms_recipients_list(value, article)
if sms_recipients.blank?
logger.debug "No SMS recipients found for Ticket# #{number}"
return
end
sms_recipients_to = sms_recipients
.map { |recipient| "#{recipient.fullname} (#{recipient.mobile})" }
.join(', ')
channel = Channel.find_by(area: 'Sms::Notification')
if !channel.active?
# write info message since we have an active trigger
logger.info "Found possible SMS recipient(s) (#{sms_recipients_to}) for Ticket# #{number} but SMS channel is not active."
return
end
objects = build_notification_template_objects(article)
body = NotificationFactory::Renderer.new(
objects: objects,
template: value['body'],
escape: false
).render.html2text.tr(' ', ' ') # convert non-breaking space to simple space
# attributes content_type is not needed for SMS
Ticket::Article.create(
ticket_id: id,
subject: 'SMS notification',
to: sms_recipients_to,
body: body,
internal: value['internal'] || false, # default to public if value was not set
sender: Ticket::Article::Sender.find_by(name: 'System'),
type: Ticket::Article::Type.find_by(name: 'sms'),
preferences: {
perform_origin: perform_origin,
sms_recipients: sms_recipients.map(&:mobile),
channel_id: channel.id,
},
updated_by_id: 1,
created_by_id: 1,
)
end
def trigger_based_notification?(user)
blocked_in_days = trigger_based_notification_blocked_in_days(user)
return true if blocked_in_days.zero?
logger.info "Send no trigger based notification to #{user.email} because email is marked as mail_delivery_failed for #{blocked_in_days} day(s)"
false
end
def trigger_based_notification_blocked_in_days(user)
return 0 if !user.preferences[:mail_delivery_failed]
return 0 if user.preferences[:mail_delivery_failed_data].blank?
# blocked for 60 full days
(user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
end
end