ticket.rb 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. class Ticket < ApplicationModel
  3. include CanBeImported
  4. include HasActivityStreamLog
  5. include ChecksClientNotification
  6. include CanCsvImport
  7. include ChecksHtmlSanitized
  8. include ChecksHumanChanges
  9. include HasHistory
  10. include HasTags
  11. include HasSearchIndexBackend
  12. include HasOnlineNotifications
  13. include HasLinks
  14. include HasObjectManagerAttributes
  15. include HasTaskbars
  16. include Ticket::CallsStatsTicketReopenLog
  17. include Ticket::EnqueuesUserTicketCounterJob
  18. include Ticket::ResetsPendingTimeSeconds
  19. include Ticket::SetsCloseTime
  20. include Ticket::SetsOnlineNotificationSeen
  21. include Ticket::TouchesAssociations
  22. include Ticket::TriggersSubscriptions
  23. include Ticket::ChecksReopenAfterCertainTime
  24. include ::Ticket::Escalation
  25. include ::Ticket::Subject
  26. include ::Ticket::Assets
  27. include ::Ticket::SearchIndex
  28. include ::Ticket::Search
  29. include ::Ticket::MergeHistory
  30. store :preferences
  31. after_initialize :check_defaults, if: :new_record?
  32. before_create :check_generate, :check_defaults, :check_title, :set_default_state, :set_default_priority
  33. before_update :check_defaults, :check_title, :reset_pending_time, :check_owner_active
  34. # This must be loaded late as it depends on the internal before_create and before_update handlers of ticket.rb.
  35. include Ticket::SetsLastOwnerUpdateTime
  36. # workflow checks should run after before_create and before_update callbacks
  37. # the transaction dispatcher must be run after the workflow checks!
  38. include ChecksCoreWorkflow
  39. include HasTransactionDispatcher
  40. validates :group_id, presence: true
  41. activity_stream_permission 'ticket.agent'
  42. core_workflow_screens 'create_middle', 'edit', 'overview_bulk'
  43. core_workflow_admin_screens 'create_middle', 'edit'
  44. activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update
  45. :create_article_type_id,
  46. :create_article_sender_id,
  47. :article_count,
  48. :first_response_at,
  49. :first_response_escalation_at,
  50. :first_response_in_min,
  51. :first_response_diff_in_min,
  52. :close_at,
  53. :close_escalation_at,
  54. :close_in_min,
  55. :close_diff_in_min,
  56. :update_escalation_at,
  57. :update_in_min,
  58. :update_diff_in_min,
  59. :last_close_at,
  60. :last_contact_at,
  61. :last_contact_agent_at,
  62. :last_contact_customer_at,
  63. :last_owner_update_at,
  64. :preferences
  65. search_index_attributes_relevant :organization_id,
  66. :group_id,
  67. :state_id,
  68. :priority_id
  69. history_attributes_ignored :create_article_type_id,
  70. :create_article_sender_id,
  71. :article_count,
  72. :preferences
  73. history_relation_object 'Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom'
  74. validates :note, length: { maximum: 250 }
  75. sanitized_html :note
  76. belongs_to :group, optional: true
  77. belongs_to :organization, optional: true
  78. has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy, inverse_of: :ticket
  79. has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
  80. has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
  81. has_many :mentions, as: :mentionable, dependent: :destroy
  82. has_one :shared_draft, class_name: 'Ticket::SharedDraftZoom', inverse_of: :ticket, dependent: :destroy
  83. belongs_to :state, class_name: 'Ticket::State', optional: true
  84. belongs_to :priority, class_name: 'Ticket::Priority', optional: true
  85. belongs_to :owner, class_name: 'User', optional: true
  86. belongs_to :customer, class_name: 'User', optional: true
  87. belongs_to :created_by, class_name: 'User', optional: true
  88. belongs_to :updated_by, class_name: 'User', optional: true
  89. belongs_to :create_article_type, class_name: 'Ticket::Article::Type', optional: true
  90. belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender', optional: true
  91. association_attributes_ignored :flags, :mentions
  92. attr_accessor :callback_loop
  93. =begin
  94. processes tickets which have reached their pending time and sets next state_id
  95. processed_tickets = Ticket.process_pending
  96. returns
  97. processed_tickets = [<Ticket>, ...]
  98. =end
  99. def self.process_pending
  100. result = []
  101. # process pending action tickets
  102. pending_action = Ticket::StateType.find_by(name: 'pending action')
  103. ticket_states_pending_action = Ticket::State.where(state_type_id: pending_action)
  104. .where.not(next_state_id: nil)
  105. if ticket_states_pending_action.present?
  106. next_state_map = {}
  107. ticket_states_pending_action.each do |state|
  108. next_state_map[state.id] = state.next_state_id
  109. end
  110. tickets = where(state_id: next_state_map.keys)
  111. .where('pending_time <= ?', Time.zone.now)
  112. tickets.find_each(batch_size: 500) do |ticket|
  113. Transaction.execute do
  114. ticket.state_id = next_state_map[ticket.state_id]
  115. ticket.updated_at = Time.zone.now
  116. ticket.updated_by_id = 1
  117. ticket.save!
  118. end
  119. result.push ticket
  120. end
  121. end
  122. # process pending reminder tickets
  123. pending_reminder = Ticket::StateType.find_by(name: 'pending reminder')
  124. ticket_states_pending_reminder = Ticket::State.where(state_type_id: pending_reminder)
  125. if ticket_states_pending_reminder.present?
  126. reminder_state_map = {}
  127. ticket_states_pending_reminder.each do |state|
  128. reminder_state_map[state.id] = state.next_state_id
  129. end
  130. tickets = where(state_id: reminder_state_map.keys)
  131. .where('pending_time <= ?', Time.zone.now)
  132. tickets.find_each(batch_size: 500) do |ticket|
  133. article_id = nil
  134. article = Ticket::Article.last_customer_agent_article(ticket.id)
  135. if article
  136. article_id = article.id
  137. end
  138. # send notification
  139. TransactionJob.perform_now(
  140. object: 'Ticket',
  141. type: 'reminder_reached',
  142. object_id: ticket.id,
  143. article_id: article_id,
  144. user_id: 1,
  145. )
  146. result.push ticket
  147. end
  148. end
  149. result
  150. end
  151. def auto_assign(user)
  152. return if !persisted?
  153. return if Setting.get('ticket_auto_assignment').blank?
  154. return if owner_id != 1
  155. return if !TicketPolicy.new(user, self).full?
  156. user_ids_ignore = Array(Setting.get('ticket_auto_assignment_user_ids_ignore')).map(&:to_i)
  157. return if user_ids_ignore.include?(user.id)
  158. ticket_auto_assignment_selector = Setting.get('ticket_auto_assignment_selector')
  159. return if ticket_auto_assignment_selector.blank?
  160. condition = ticket_auto_assignment_selector[:condition].merge(
  161. 'ticket.id' => {
  162. 'operator' => 'is',
  163. 'value' => id,
  164. }
  165. )
  166. ticket_count, = Ticket.selectors(condition, limit: 1, current_user: user, access: 'full')
  167. return if ticket_count.to_i.zero?
  168. update!(owner: user)
  169. end
  170. =begin
  171. processes escalated tickets
  172. processed_tickets = Ticket.process_escalation
  173. returns
  174. processed_tickets = [<Ticket>, ...]
  175. =end
  176. def self.process_escalation
  177. result = []
  178. # fetch all escalated and soon to be escalating tickets
  179. where('escalation_at <= ?', 15.minutes.from_now).find_each(batch_size: 500) do |ticket|
  180. article_id = nil
  181. article = Ticket::Article.last_customer_agent_article(ticket.id)
  182. if article
  183. article_id = article.id
  184. end
  185. # send escalation
  186. if ticket.escalation_at < Time.zone.now
  187. TransactionJob.perform_now(
  188. object: 'Ticket',
  189. type: 'escalation',
  190. object_id: ticket.id,
  191. article_id: article_id,
  192. user_id: 1,
  193. )
  194. result.push ticket
  195. next
  196. end
  197. # check if warning needs to be sent
  198. TransactionJob.perform_now(
  199. object: 'Ticket',
  200. type: 'escalation_warning',
  201. object_id: ticket.id,
  202. article_id: article_id,
  203. user_id: 1,
  204. )
  205. result.push ticket
  206. end
  207. result
  208. end
  209. =begin
  210. processes tickets which auto unassign time has reached
  211. processed_tickets = Ticket.process_auto_unassign
  212. returns
  213. processed_tickets = [<Ticket>, ...]
  214. =end
  215. def self.process_auto_unassign
  216. # process pending action tickets
  217. state_ids = Ticket::State.by_category(:work_on).pluck(:id)
  218. return [] if state_ids.blank?
  219. result = []
  220. groups = Group.where(active: true).where('assignment_timeout IS NOT NULL AND groups.assignment_timeout != 0')
  221. return [] if groups.blank?
  222. groups.each do |group|
  223. next if group.assignment_timeout.blank?
  224. 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)
  225. ticket_ids.each do |ticket_id|
  226. ticket = Ticket.find_by(id: ticket_id)
  227. next if !ticket
  228. minutes_since_last_assignment = Time.zone.now - ticket.last_owner_update_at
  229. next if (minutes_since_last_assignment / 60) <= group.assignment_timeout
  230. Transaction.execute do
  231. ticket.owner_id = 1
  232. ticket.updated_at = Time.zone.now
  233. ticket.updated_by_id = 1
  234. ticket.save!
  235. end
  236. result.push ticket
  237. end
  238. end
  239. result
  240. end
  241. =begin
  242. merge tickets
  243. ticket = Ticket.find(123)
  244. result = ticket.merge_to(
  245. ticket_id: 123,
  246. user_id: 123,
  247. )
  248. returns
  249. result = true|false
  250. =end
  251. def merge_to(data)
  252. # prevent cross merging tickets
  253. target_ticket = Ticket.find_by(id: data[:ticket_id])
  254. raise 'no target ticket given' if !target_ticket
  255. raise Exceptions::UnprocessableEntity, __('It is not possible to merge into an already merged ticket.') if target_ticket.state.state_type.name == 'merged'
  256. # check different ticket ids
  257. raise Exceptions::UnprocessableEntity, __('A ticket cannot be merged into itself.') if id == target_ticket.id
  258. # update articles
  259. Transaction.execute context: 'merge' do
  260. Ticket::Article.where(ticket_id: id).each(&:touch)
  261. # quiet update of reassign of articles
  262. Ticket::Article.where(ticket_id: id).update_all(['ticket_id = ?', data[:ticket_id]]) # rubocop:disable Rails/SkipsModelValidations
  263. # mark target ticket as updated
  264. # otherwise the "received_merge" history entry
  265. # will be the same as the last updated_at
  266. # which might be a long time ago
  267. target_ticket.updated_at = Time.zone.now
  268. # add merge event to both ticket's history (Issue #2469 - Add information "Ticket merged" to History)
  269. target_ticket.history_log(
  270. 'received_merge',
  271. data[:user_id],
  272. id_to: target_ticket.id,
  273. id_from: id,
  274. )
  275. history_log(
  276. 'merged_into',
  277. data[:user_id],
  278. id_to: target_ticket.id,
  279. id_from: id,
  280. )
  281. # create new merge article
  282. Ticket::Article.create(
  283. ticket_id: id,
  284. type_id: Ticket::Article::Type.lookup(name: 'note').id,
  285. sender_id: Ticket::Article::Sender.lookup(name: 'Agent').id,
  286. body: 'merged',
  287. internal: false,
  288. created_by_id: data[:user_id],
  289. updated_by_id: data[:user_id],
  290. )
  291. # search for mention duplicates and destroy them before moving mentions
  292. Mention.duplicates(self, target_ticket).destroy_all
  293. Mention.where(mentionable: self).update_all(mentionable_id: target_ticket.id) # rubocop:disable Rails/SkipsModelValidations
  294. # reassign links to the new ticket
  295. # rubocop:disable Rails/SkipsModelValidations
  296. ticket_source_id = Link::Object.find_by(name: 'Ticket').id
  297. # search for all duplicate source and target links and destroy them
  298. # before link merging
  299. Link.duplicates(
  300. object1_id: ticket_source_id,
  301. object1_value: id,
  302. object2_value: data[:ticket_id]
  303. ).destroy_all
  304. Link.where(
  305. link_object_source_id: ticket_source_id,
  306. link_object_source_value: id,
  307. ).update_all(link_object_source_value: data[:ticket_id])
  308. Link.where(
  309. link_object_target_id: ticket_source_id,
  310. link_object_target_value: id,
  311. ).update_all(link_object_target_value: data[:ticket_id])
  312. # rubocop:enable Rails/SkipsModelValidations
  313. # link tickets
  314. Link.add(
  315. link_type: 'parent',
  316. link_object_source: 'Ticket',
  317. link_object_source_value: data[:ticket_id],
  318. link_object_target: 'Ticket',
  319. link_object_target_value: id
  320. )
  321. # external sync references
  322. ExternalSync.migrate('Ticket', id, target_ticket.id)
  323. # set state to 'merged'
  324. self.state_id = Ticket::State.lookup(name: 'merged').id
  325. # rest owner
  326. self.owner_id = 1
  327. # save ticket
  328. save!
  329. # touch new ticket (to broadcast change)
  330. target_ticket.touch # rubocop:disable Rails/SkipsModelValidations
  331. EventBuffer.add('transaction', {
  332. object: target_ticket.class.name,
  333. type: 'update.received_merge',
  334. data: target_ticket,
  335. changes: {},
  336. id: target_ticket.id,
  337. user_id: UserInfo.current_user_id,
  338. created_at: Time.zone.now,
  339. })
  340. EventBuffer.add('transaction', {
  341. object: self.class.name,
  342. type: 'update.merged_into',
  343. data: self,
  344. changes: {},
  345. id: id,
  346. user_id: UserInfo.current_user_id,
  347. created_at: Time.zone.now,
  348. })
  349. end
  350. true
  351. end
  352. =begin
  353. get count of tickets and tickets which match on selector
  354. @param [Hash] selectors hash with conditions
  355. @oparam [Hash] options
  356. @option options [String] :access can be 'full', 'read', 'create' or 'ignore' (ignore means a selector over all tickets), defaults to 'full'
  357. @option options [Integer] :limit of tickets to return
  358. @option options [User] :user is a current user
  359. @option options [Integer] :execution_time is a current user
  360. @return [Integer, [<Ticket>]]
  361. @example
  362. ticket_count, tickets = Ticket.selectors(params[:condition], limit: limit, current_user: current_user, access: 'full')
  363. ticket_count # count of found tickets
  364. tickets # tickets
  365. =end
  366. def self.selectors(selectors, options)
  367. limit = options[:limit] || 10
  368. current_user = options[:current_user]
  369. access = options[:access] || 'full'
  370. raise 'no selectors given' if !selectors
  371. query, bind_params, tables = selector2sql(selectors, options)
  372. return [] if !query
  373. ActiveRecord::Base.transaction(requires_new: true) do
  374. if !current_user || access == 'ignore'
  375. ticket_count = Ticket.distinct.where(query, *bind_params).joins(tables).reorder(options[:order_by]).count
  376. tickets = Ticket.distinct.where(query, *bind_params).joins(tables).reorder(options[:order_by]).limit(limit)
  377. next [ticket_count, tickets]
  378. end
  379. tickets = "TicketPolicy::#{access.camelize}Scope".constantize
  380. .new(current_user).resolve
  381. .distinct
  382. .where(query, *bind_params)
  383. .joins(tables)
  384. .reorder(options[:order_by])
  385. next [tickets.count, tickets.limit(limit)]
  386. rescue ActiveRecord::StatementInvalid => e
  387. Rails.logger.error e
  388. raise ActiveRecord::Rollback
  389. end
  390. end
  391. =begin
  392. generate condition query to search for tickets based on condition
  393. query_condition, bind_condition, tables = selector2sql(params[:condition], current_user: current_user)
  394. condition example
  395. {
  396. 'ticket.title' => {
  397. operator: 'contains', # contains not
  398. value: 'some value',
  399. },
  400. 'ticket.state_id' => {
  401. operator: 'is',
  402. value: [1,2,5]
  403. },
  404. 'ticket.created_at' => {
  405. operator: 'after (absolute)', # after,before
  406. value: '2015-10-17T06:00:00.000Z',
  407. },
  408. 'ticket.created_at' => {
  409. operator: 'within next (relative)', # within next, within last, after, before
  410. range: 'day', # minute|hour|day|month|year
  411. value: '25',
  412. },
  413. 'ticket.owner_id' => {
  414. operator: 'is', # is not
  415. pre_condition: 'current_user.id',
  416. },
  417. 'ticket.owner_id' => {
  418. operator: 'is', # is not
  419. pre_condition: 'specific',
  420. value: 4711,
  421. },
  422. 'ticket.escalation_at' => {
  423. operator: 'is not', # not
  424. value: nil,
  425. },
  426. 'ticket.tags' => {
  427. operator: 'contains all', # contains all|contains one|contains all not|contains one not
  428. value: 'tag1, tag2',
  429. },
  430. }
  431. =end
  432. def self.selector2sql(selectors, options = {})
  433. Ticket::Selector::Sql.new(selector: selectors, options: options).get
  434. end
  435. =begin
  436. perform changes on ticket
  437. ticket.perform_changes(trigger, 'trigger', item, current_user_id)
  438. # or
  439. ticket.perform_changes(job, 'job', item, current_user_id)
  440. =end
  441. def perform_changes(performable, perform_origin, item = nil, current_user_id = nil, activator_type: nil)
  442. return if performable.try(:performable_on?, self, activator_type:) === false # rubocop:disable Style/CaseEquality
  443. perform = performable.perform
  444. logger.debug { "Perform #{perform_origin} #{perform.inspect} on Ticket.find(#{id})" }
  445. article = begin
  446. Ticket::Article.find_by(id: item.try(:dig, :article_id))
  447. rescue ArgumentError
  448. nil
  449. end
  450. # if the configuration contains the deletion of the ticket then
  451. # we skip all other ticket changes because they does not matter
  452. if perform['ticket.action'].present? && perform['ticket.action']['value'] == 'delete'
  453. perform.each_key do |key|
  454. (object_name, attribute) = key.split('.', 2)
  455. next if object_name != 'ticket'
  456. next if attribute == 'action'
  457. perform.delete(key)
  458. end
  459. end
  460. objects = build_notification_template_objects(article)
  461. perform_notification = {}
  462. perform_article = {}
  463. changed = false
  464. perform.each do |key, value|
  465. (object_name, attribute) = key.split('.', 2)
  466. 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'
  467. # send notification/create article (after changes are done)
  468. if object_name == 'article'
  469. perform_article[key] = value
  470. next
  471. end
  472. if object_name == 'notification'
  473. perform_notification[key] = value
  474. next
  475. end
  476. # Apply pending_time changes
  477. if perform_changes_date(object_name: object_name, attribute: attribute, value: value, performable: performable)
  478. changed = true
  479. next
  480. end
  481. # update tags
  482. if key == 'ticket.tags'
  483. next if value['value'].blank?
  484. tags = value['value'].split(',')
  485. case value['operator']
  486. when 'add'
  487. tags.each do |tag|
  488. tag_add(tag, current_user_id || 1, sourceable: performable)
  489. end
  490. when 'remove'
  491. tags.each do |tag|
  492. tag_remove(tag, current_user_id || 1, sourceable: performable)
  493. end
  494. else
  495. logger.error "Unknown #{attribute} operator #{value['operator']}"
  496. end
  497. next
  498. end
  499. # delete ticket
  500. if key == 'ticket.action'
  501. next if value['value'].blank?
  502. next if value['value'] != 'delete'
  503. logger.info { "Deleted ticket from #{perform_origin} #{perform.inspect} Ticket.find(#{id})" }
  504. destroy!
  505. next
  506. end
  507. # lookup pre_condition
  508. if value['pre_condition']
  509. if value['pre_condition'].start_with?('not_set')
  510. value['value'] = 1
  511. elsif value['pre_condition'].start_with?('current_user.')
  512. raise __("The required parameter 'current_user_id' is missing.") if !current_user_id
  513. value['value'] = current_user_id
  514. end
  515. end
  516. # update ticket
  517. next if self[attribute].to_s == value['value'].to_s
  518. changed = true
  519. if value['value'].is_a?(String)
  520. value['value'] = NotificationFactory::Mailer.template(
  521. templateInline: value['value'],
  522. objects: objects,
  523. quote: true,
  524. )
  525. end
  526. self[attribute] = value['value']
  527. history_change_source_attribute(performable, attribute)
  528. logger.debug { "set #{object_name}.#{attribute} = #{value['value'].inspect} for ticket_id #{id}" }
  529. end
  530. if changed
  531. save!
  532. end
  533. perform_article.each do |key, value|
  534. raise __("Article could not be created. An unsupported key other than 'article.note' was provided.") if key != 'article.note'
  535. add_trigger_note(id, value, objects, perform_origin, performable)
  536. end
  537. perform_notification.each do |key, value|
  538. # send notification
  539. case key
  540. when 'notification.sms'
  541. send_sms_notification(value, article, perform_origin, performable)
  542. next
  543. when 'notification.email'
  544. send_email_notification(value, article, perform_origin, performable)
  545. when 'notification.webhook'
  546. TriggerWebhookJob.perform_later(performable,
  547. self,
  548. article,
  549. changes: human_changes(
  550. item.try(:dig, :changes),
  551. self,
  552. ),
  553. user_id: item.try(:dig, :user_id),
  554. execution_type: perform_origin,
  555. event_type: item.try(:dig, :type))
  556. end
  557. end
  558. performable.try(:performed_on, self, activator_type:)
  559. true
  560. end
  561. def perform_changes_date(object_name:, attribute:, value:, performable:)
  562. return if object_name != 'ticket'
  563. object_attribute = ObjectManager::Attribute.for_object('Ticket').find_by(name: attribute, data_type: %w[datetime date])
  564. return if object_attribute.blank?
  565. new_value = if value['operator'] == 'relative'
  566. TimeRangeHelper.relative(range: value['range'], value: value['value'])
  567. else
  568. value['value']
  569. end
  570. if new_value
  571. self[attribute] = if object_attribute[:data_type] == 'datetime'
  572. new_value.to_datetime
  573. else
  574. new_value.to_date
  575. end
  576. history_change_source_attribute(performable, attribute)
  577. end
  578. true
  579. end
  580. =begin
  581. perform changes on ticket
  582. ticket.add_trigger_note(ticket_id, note, objects, perform_origin)
  583. =end
  584. def add_trigger_note(ticket_id, note, objects, perform_origin, performable)
  585. rendered_subject = NotificationFactory::Mailer.template(
  586. templateInline: note[:subject],
  587. objects: objects,
  588. quote: true,
  589. )
  590. rendered_body = NotificationFactory::Mailer.template(
  591. templateInline: note[:body],
  592. objects: objects,
  593. quote: true,
  594. )
  595. article = Ticket::Article.new(
  596. ticket_id: ticket_id,
  597. subject: rendered_subject,
  598. content_type: 'text/html',
  599. body: rendered_body,
  600. internal: note[:internal],
  601. sender: Ticket::Article::Sender.find_by(name: 'System'),
  602. type: Ticket::Article::Type.find_by(name: 'note'),
  603. preferences: {
  604. perform_origin: perform_origin,
  605. notification: true,
  606. },
  607. updated_by_id: 1,
  608. created_by_id: 1,
  609. )
  610. article.history_change_source_attribute(performable, 'created')
  611. article.save!
  612. end
  613. =begin
  614. perform active triggers on ticket
  615. Ticket.perform_triggers(ticket, article, triggers, item, triggers, options)
  616. =end
  617. def self.perform_triggers(ticket, article, triggers, item, options = {})
  618. recursive = Setting.get('ticket_trigger_recursive')
  619. type = options[:type] || item[:type]
  620. local_options = options.clone
  621. local_options[:type] = type
  622. local_options[:reset_user_id] = true
  623. local_options[:disable] = ['Transaction::Notification']
  624. local_options[:trigger_ids] ||= {}
  625. local_options[:trigger_ids][ticket.id.to_s] ||= []
  626. local_options[:loop_count] ||= 0
  627. local_options[:loop_count] += 1
  628. ticket_trigger_recursive_max_loop = Setting.get('ticket_trigger_recursive_max_loop')&.to_i || 10
  629. if local_options[:loop_count] > ticket_trigger_recursive_max_loop
  630. message = "Stopped perform_triggers for this object (Ticket/#{ticket.id}), because loop count was #{local_options[:loop_count]}!"
  631. logger.info { message }
  632. return [false, message]
  633. end
  634. return [true, __('No triggers active')] if triggers.blank?
  635. # check if notification should be send because of customer emails
  636. send_notification = true
  637. if local_options[:send_notification] == false
  638. send_notification = false
  639. elsif item[:article_id]
  640. article = Ticket::Article.lookup(id: item[:article_id])
  641. if article&.preferences && article.preferences['send-auto-response'] == false
  642. send_notification = false
  643. end
  644. end
  645. Transaction.execute(local_options) do
  646. triggers.each do |trigger|
  647. logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  648. user_id = ticket.updated_by_id
  649. if article
  650. user_id = article.updated_by_id
  651. end
  652. user = User.lookup(id: user_id)
  653. # verify is condition is matching
  654. ticket_count, tickets = Ticket.selectors(
  655. trigger.condition,
  656. limit: 1,
  657. execution_time: true,
  658. current_user: user,
  659. access: 'ignore',
  660. ticket_action: type,
  661. ticket_id: ticket.id,
  662. article_id: article&.id,
  663. changes: item[:changes],
  664. changes_required: trigger.condition_changes_required?
  665. )
  666. next if ticket_count.blank?
  667. next if ticket_count.zero?
  668. next if tickets.take.id != ticket.id
  669. if recursive == false && local_options[:loop_count] > 1
  670. 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."
  671. logger.info { message }
  672. return [true, message]
  673. end
  674. if article && send_notification == false && trigger.perform['notification.email'] && trigger.perform['notification.email']['recipient']
  675. recipient = trigger.perform['notification.email']['recipient']
  676. local_options[:send_notification] = false
  677. if recipient.include?('ticket_customer') || recipient.include?('article_last_sender')
  678. 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})" }
  679. next
  680. end
  681. end
  682. if local_options[:trigger_ids][ticket.id.to_s].include?(trigger.id)
  683. logger.info { "Skip trigger (#{trigger.name}/#{trigger.id}) because was already executed for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  684. next
  685. end
  686. local_options[:trigger_ids][ticket.id.to_s].push trigger.id
  687. logger.info { "Execute trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
  688. ticket.perform_changes(trigger, 'trigger', item, user_id, activator_type: type)
  689. if recursive == true
  690. TransactionDispatcher.commit(local_options)
  691. end
  692. end
  693. end
  694. [true, ticket, local_options]
  695. end
  696. =begin
  697. get all email references headers of a ticket, to exclude some, parse it as array into method
  698. references = ticket.get_references
  699. result
  700. ['message-id-1234', 'message-id-5678']
  701. ignore references header(s)
  702. references = ticket.get_references(['message-id-5678'])
  703. result
  704. ['message-id-1234']
  705. =end
  706. def get_references(ignore = [])
  707. references = []
  708. Ticket::Article.select('in_reply_to, message_id').where(ticket_id: id).each do |article|
  709. if article.in_reply_to.present?
  710. references.push article.in_reply_to
  711. end
  712. next if article.message_id.blank?
  713. references.push article.message_id
  714. end
  715. ignore.each do |item|
  716. references.delete(item)
  717. end
  718. references
  719. end
  720. =begin
  721. get all articles of a ticket in correct order (overwrite active record default method)
  722. articles = ticket.articles
  723. result
  724. [article1, article2]
  725. =end
  726. def articles
  727. Ticket::Article.where(ticket_id: id).reorder(:created_at, :id)
  728. end
  729. # Get whichever #last_contact_* was later
  730. # This is not identical to #last_contact_at
  731. # It returns time to last original (versus follow up) contact
  732. # @return [Time, nil]
  733. def last_original_update_at
  734. [last_contact_agent_at, last_contact_customer_at].compact.max
  735. end
  736. # true if conversation did happen and agent responded
  737. # false if customer is waiting for response or agent reached out and customer did not respond yet
  738. # @return [Bool]
  739. def agent_responded?
  740. return false if last_contact_customer_at.blank?
  741. return false if last_contact_agent_at.blank?
  742. last_contact_customer_at < last_contact_agent_at
  743. end
  744. =begin
  745. Get the color of the state the current ticket is in
  746. ticket.current_state_color
  747. returns a hex color code
  748. =end
  749. def current_state_color
  750. return '#f35912' if escalation_at && escalation_at < Time.zone.now
  751. case state.state_type.name
  752. when 'new', 'open'
  753. return '#faab00'
  754. when 'closed'
  755. return '#38ad69'
  756. when 'pending reminder'
  757. return '#faab00' if pending_time && pending_time < Time.zone.now
  758. end
  759. '#000000'
  760. end
  761. def mention_user_ids
  762. mentions.pluck(:user_id)
  763. end
  764. private
  765. def check_generate
  766. return true if number
  767. self.number = Ticket::Number.generate
  768. true
  769. end
  770. def check_title
  771. return true if !title
  772. title.gsub!(%r{\s|\t|\r}, ' ')
  773. true
  774. end
  775. def check_defaults
  776. check_default_owner
  777. check_default_organization
  778. true
  779. end
  780. def check_default_owner
  781. return if !has_attribute?(:owner_id)
  782. return if owner_id || owner
  783. self.owner_id = 1
  784. end
  785. def check_default_organization
  786. return if !has_attribute?(:organization_id)
  787. return if !customer_id
  788. customer = User.find_by(id: customer_id)
  789. return if !customer
  790. return if organization_id.present? && customer.organization_id?(organization_id)
  791. return if organization.present? && customer.organization_id?(organization.id)
  792. self.organization_id = customer.organization_id
  793. end
  794. def reset_pending_time
  795. # ignore if no state has changed
  796. return true if !changes_to_save['state_id']
  797. # ignore if new state is blank and
  798. # let handle ActiveRecord the error
  799. return if state_id.blank?
  800. # check if new state isn't pending*
  801. current_state = Ticket::State.lookup(id: state_id)
  802. current_state_type = Ticket::StateType.lookup(id: current_state.state_type_id)
  803. # in case, set pending_time to nil
  804. return true if current_state_type.name.match?(%r{^pending}i)
  805. self.pending_time = nil
  806. true
  807. end
  808. def set_default_state
  809. return true if state_id
  810. default_ticket_state = Ticket::State.find_by(default_create: true)
  811. return true if !default_ticket_state
  812. self.state_id = default_ticket_state.id
  813. true
  814. end
  815. def set_default_priority
  816. return true if priority_id
  817. default_ticket_priority = Ticket::Priority.find_by(default_create: true)
  818. return true if !default_ticket_priority
  819. self.priority_id = default_ticket_priority.id
  820. true
  821. end
  822. def check_owner_active
  823. return true if Setting.get('import_mode')
  824. # only change the owner for non closed Tickets for historical/reporting reasons
  825. return true if state.present? && Ticket::StateType.lookup(id: state.state_type_id)&.name == 'closed'
  826. # return when ticket is unassigned
  827. return true if owner_id.blank?
  828. return true if owner_id == 1
  829. # return if owner is active, is agent and has access to group of ticket
  830. return true if owner.active? && owner.permissions?('ticket.agent') && owner.group_access?(group_id, 'full')
  831. # else set the owner of the ticket to the default user as unassigned
  832. self.owner_id = 1
  833. true
  834. end
  835. # articles.last breaks (returns the wrong article)
  836. # if another email notification trigger preceded this one
  837. # (see https://github.com/zammad/zammad/issues/1543)
  838. def build_notification_template_objects(article)
  839. last_article = nil
  840. last_internal_article = nil
  841. last_external_article = nil
  842. all_articles = articles
  843. if article.nil?
  844. last_article = all_articles.last
  845. last_internal_article = all_articles.reverse.find(&:internal?)
  846. last_external_article = all_articles.reverse.find { |a| !a.internal? }
  847. else
  848. last_article = article
  849. last_internal_article = article.internal? ? article : all_articles.reverse.find(&:internal?)
  850. last_external_article = article.internal? ? all_articles.reverse.find { |a| !a.internal? } : article
  851. end
  852. {
  853. ticket: self,
  854. article: last_article,
  855. last_article: last_article,
  856. last_internal_article: last_internal_article,
  857. last_external_article: last_external_article,
  858. created_article: article,
  859. created_internal_article: article&.internal? ? article : nil,
  860. created_external_article: article&.internal? ? nil : article,
  861. }
  862. end
  863. def send_email_notification(value, article, perform_origin, performable)
  864. # value['recipient'] was a string in the past (single-select) so we convert it to array if needed
  865. value_recipient = Array(value['recipient'])
  866. recipients_raw = []
  867. value_recipient.each do |recipient|
  868. case recipient
  869. when 'article_last_sender'
  870. if article.present?
  871. if article.reply_to.present?
  872. recipients_raw.push(article.reply_to)
  873. elsif article.from.present?
  874. recipients_raw.push(article.from)
  875. elsif article.origin_by_id
  876. email = User.find_by(id: article.origin_by_id).email
  877. recipients_raw.push(email)
  878. elsif article.created_by_id
  879. email = User.find_by(id: article.created_by_id).email
  880. recipients_raw.push(email)
  881. end
  882. end
  883. when 'ticket_customer'
  884. email = User.find_by(id: customer_id).email
  885. recipients_raw.push(email)
  886. when 'ticket_owner'
  887. email = User.find_by(id: owner_id).email
  888. recipients_raw.push(email)
  889. when 'ticket_agents'
  890. User.group_access(group_id, 'full').sort_by(&:login).each do |user|
  891. recipients_raw.push(user.email)
  892. end
  893. when %r{\Auserid_(\d+)\z}
  894. user = User.lookup(id: $1)
  895. if !user
  896. logger.warn "Can't find configured Trigger Email recipient User with ID '#{$1}'"
  897. next
  898. end
  899. recipients_raw.push(user.email)
  900. else
  901. logger.error "Unknown email notification recipient '#{recipient}'"
  902. next
  903. end
  904. end
  905. recipients_checked = []
  906. recipients_raw.each do |recipient_email|
  907. users = User.where(email: recipient_email)
  908. next if users.any? { |user| !trigger_based_notification?(user) }
  909. # send notifications only to email addresses
  910. next if recipient_email.blank?
  911. # check if address is valid
  912. begin
  913. Mail::AddressList.new(recipient_email).addresses.each do |address|
  914. recipient_email = address.address
  915. email_address_validation = EmailAddressValidation.new(recipient_email)
  916. break if recipient_email.present? && email_address_validation.valid?
  917. end
  918. rescue
  919. if recipient_email.present?
  920. if recipient_email !~ %r{^(.+?)<(.+?)@(.+?)>$}
  921. next # no usable format found
  922. end
  923. recipient_email = "#{$2}@#{$3}" # rubocop:disable Lint/OutOfRangeRegexpRef
  924. end
  925. end
  926. email_address_validation = EmailAddressValidation.new(recipient_email)
  927. next if !email_address_validation.valid?
  928. # do not send notification if system address
  929. next if EmailAddress.exists?(email: recipient_email.downcase)
  930. # do not sent notifications to this recipients
  931. send_no_auto_response_reg_exp = Setting.get('send_no_auto_response_reg_exp')
  932. begin
  933. next if recipient_email.match?(%r{#{send_no_auto_response_reg_exp}}i)
  934. rescue => e
  935. logger.error "Invalid regex '#{send_no_auto_response_reg_exp}' in setting send_no_auto_response_reg_exp"
  936. logger.error e
  937. next if recipient_email.match?(%r{(mailer-daemon|postmaster|abuse|root|noreply|noreply.+?|no-reply|no-reply.+?)@.+?}i)
  938. end
  939. # check if notification should be send because of customer emails
  940. if article.present? && article.preferences.fetch('is-auto-response', false) == true && article.from && article.from =~ %r{#{Regexp.quote(recipient_email)}}i
  941. logger.info "Send no trigger based notification to #{recipient_email} because of auto response tagged incoming email"
  942. next
  943. end
  944. # loop protection / check if maximal count of trigger mail has reached
  945. map = {
  946. 10 => 10,
  947. 30 => 15,
  948. 60 => 25,
  949. 180 => 50,
  950. 600 => 100,
  951. }
  952. skip = false
  953. map.each do |minutes, count|
  954. already_sent = Ticket::Article.where(
  955. ticket_id: id,
  956. sender: Ticket::Article::Sender.find_by(name: 'System'),
  957. type: Ticket::Article::Type.find_by(name: 'email'),
  958. ).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{SqlHelper.quote_like(recipient_email.strip)}%").count
  959. next if already_sent < count
  960. logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} for this ticket within last #{minutes} minutes (loop protection)"
  961. skip = true
  962. break
  963. end
  964. next if skip
  965. map = {
  966. 10 => 30,
  967. 30 => 60,
  968. 60 => 120,
  969. 180 => 240,
  970. 600 => 360,
  971. }
  972. skip = false
  973. map.each do |minutes, count|
  974. already_sent = Ticket::Article.where(
  975. sender: Ticket::Article::Sender.find_by(name: 'System'),
  976. type: Ticket::Article::Type.find_by(name: 'email'),
  977. ).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{SqlHelper.quote_like(recipient_email.strip)}%").count
  978. next if already_sent < count
  979. logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{minutes} minutes (loop protection)"
  980. skip = true
  981. break
  982. end
  983. next if skip
  984. email = recipient_email.downcase.strip
  985. next if recipients_checked.include?(email)
  986. recipients_checked.push(email)
  987. end
  988. return if recipients_checked.blank?
  989. recipient_string = recipients_checked.join(', ')
  990. group_id = self.group_id
  991. return if !group_id
  992. email_address = Group.find(group_id).email_address
  993. if !email_address
  994. logger.info "Unable to send trigger based notification to #{recipient_string} because no email address is set for group '#{group.name}'"
  995. return
  996. end
  997. if !email_address.channel_id
  998. 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})"
  999. return
  1000. end
  1001. security = nil
  1002. if Setting.get('smime_integration')
  1003. sign = value['sign'].present? && value['sign'] != 'no'
  1004. encryption = value['encryption'].present? && value['encryption'] != 'no'
  1005. security = SecureMailing::SMIME::NotificationOptions.process(
  1006. from: email_address,
  1007. recipients: recipients_checked,
  1008. perform: {
  1009. sign: sign,
  1010. encrypt: encryption,
  1011. },
  1012. )
  1013. if sign && value['sign'] == 'discard' && !security[:sign][:success]
  1014. 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)."
  1015. return
  1016. end
  1017. if encryption && value['encryption'] == 'discard' && !security[:encryption][:success]
  1018. logger.info "Unable to send trigger based notification to #{recipient_string} because public certificate is not available for encryption (discarding notification)."
  1019. return
  1020. end
  1021. end
  1022. if Setting.get('pgp_integration') && (security.nil? || (!security[:encryption][:success] && !security[:sign][:success]))
  1023. sign = value['sign'].present? && value['sign'] != 'no'
  1024. encryption = value['encryption'].present? && value['encryption'] != 'no'
  1025. security = SecureMailing::PGP::NotificationOptions.process(
  1026. from: email_address,
  1027. recipients: recipients_checked,
  1028. perform: {
  1029. sign: sign,
  1030. encrypt: encryption,
  1031. },
  1032. )
  1033. if sign && value['sign'] == 'discard' && !security[:sign][:success]
  1034. logger.info "Unable to send trigger based notification to #{recipient_string} because of missing group #{group.name} email #{email_address.email} PGP key for signing (discarding notification)."
  1035. return
  1036. end
  1037. if encryption && value['encryption'] == 'discard' && !security[:encryption][:success]
  1038. logger.info "Unable to send trigger based notification to #{recipient_string} because public PGP keys are not available for encryption (discarding notification)."
  1039. return
  1040. end
  1041. end
  1042. objects = build_notification_template_objects(article)
  1043. # get subject
  1044. subject = NotificationFactory::Mailer.template(
  1045. templateInline: value['subject'],
  1046. objects: objects,
  1047. quote: false,
  1048. )
  1049. subject = subject_build(subject)
  1050. body = NotificationFactory::Mailer.template(
  1051. templateInline: value['body'],
  1052. objects: objects,
  1053. quote: true,
  1054. )
  1055. (body, attachments_inline) = HtmlSanitizer.replace_inline_images(body, id)
  1056. preferences = {}
  1057. preferences[:perform_origin] = perform_origin
  1058. if security.present?
  1059. preferences[:security] = security
  1060. end
  1061. message = Ticket::Article.new(
  1062. ticket_id: id,
  1063. to: recipient_string,
  1064. subject: subject,
  1065. content_type: 'text/html',
  1066. body: body,
  1067. internal: value['internal'] || false, # default to public if value was not set
  1068. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1069. type: Ticket::Article::Type.find_by(name: 'email'),
  1070. preferences: preferences,
  1071. updated_by_id: 1,
  1072. created_by_id: 1,
  1073. )
  1074. message.history_change_source_attribute(performable, 'created')
  1075. message.save!
  1076. attachments_inline.each do |attachment|
  1077. Store.create!(
  1078. object: 'Ticket::Article',
  1079. o_id: message.id,
  1080. data: attachment[:data],
  1081. filename: attachment[:filename],
  1082. preferences: attachment[:preferences],
  1083. )
  1084. end
  1085. original_article = objects[:article]
  1086. if ActiveModel::Type::Boolean.new.cast(value['include_attachments']) == true && original_article&.attachments.present?
  1087. original_article.clone_attachments('Ticket::Article', message.id, only_attached_attachments: true)
  1088. end
  1089. if original_article&.should_clone_inline_attachments? # rubocop:disable Style/GuardClause
  1090. original_article.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true)
  1091. original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning
  1092. end
  1093. end
  1094. def sms_recipients_by_type(recipient_type, article)
  1095. case recipient_type
  1096. when 'article_last_sender'
  1097. return nil if article.blank?
  1098. if article.origin_by_id
  1099. article.origin_by_id
  1100. elsif article.created_by_id
  1101. article.created_by_id
  1102. end
  1103. when 'ticket_customer'
  1104. customer_id
  1105. when 'ticket_owner'
  1106. owner_id
  1107. when 'ticket_agents'
  1108. User.group_access(group_id, 'full').sort_by(&:login)
  1109. when %r{\Auserid_(\d+)\z}
  1110. return $1 if User.exists?($1)
  1111. logger.warn "Can't find configured Trigger SMS recipient User with ID '#{$1}'"
  1112. nil
  1113. else
  1114. logger.error "Unknown sms notification recipient '#{recipient}'"
  1115. nil
  1116. end
  1117. end
  1118. def build_sms_recipients_list(value, article)
  1119. Array(value['recipient'])
  1120. .each_with_object([]) { |recipient_type, sum| sum.concat(Array(sms_recipients_by_type(recipient_type, article))) }
  1121. .map { |user_or_id| user_or_id.is_a?(User) ? user_or_id : User.lookup(id: user_or_id) }
  1122. .uniq(&:id)
  1123. .select { |user| user.mobile.present? }
  1124. end
  1125. def send_sms_notification(value, article, perform_origin, performable)
  1126. sms_recipients = build_sms_recipients_list(value, article)
  1127. if sms_recipients.blank?
  1128. logger.debug "No SMS recipients found for Ticket# #{number}"
  1129. return
  1130. end
  1131. sms_recipients_to = sms_recipients
  1132. .map { |recipient| "#{recipient.fullname} (#{recipient.mobile})" }
  1133. .join(', ')
  1134. channel = Channel.find_by(area: 'Sms::Notification')
  1135. if !channel.active?
  1136. # write info message since we have an active trigger
  1137. logger.info "Found possible SMS recipient(s) (#{sms_recipients_to}) for Ticket# #{number} but SMS channel is not active."
  1138. return
  1139. end
  1140. objects = build_notification_template_objects(article)
  1141. body = NotificationFactory::Renderer.new(
  1142. objects: objects,
  1143. template: value['body'],
  1144. escape: false
  1145. ).render.html2text.tr(' ', ' ') # convert non-breaking space to simple space
  1146. # attributes content_type is not needed for SMS
  1147. article = Ticket::Article.new(
  1148. ticket_id: id,
  1149. subject: 'SMS notification',
  1150. to: sms_recipients_to,
  1151. body: body,
  1152. internal: value['internal'] || false, # default to public if value was not set
  1153. sender: Ticket::Article::Sender.find_by(name: 'System'),
  1154. type: Ticket::Article::Type.find_by(name: 'sms'),
  1155. preferences: {
  1156. perform_origin: perform_origin,
  1157. sms_recipients: sms_recipients.map(&:mobile),
  1158. channel_id: channel.id,
  1159. },
  1160. updated_by_id: 1,
  1161. created_by_id: 1,
  1162. )
  1163. article.history_change_source_attribute(performable, 'created')
  1164. article.save!
  1165. end
  1166. def trigger_based_notification?(user)
  1167. blocked_in_days = trigger_based_notification_blocked_in_days(user)
  1168. return true if blocked_in_days.zero?
  1169. logger.info "Send no trigger based notification to #{user.email} because email is marked as mail_delivery_failed for #{blocked_in_days} day(s)"
  1170. false
  1171. end
  1172. def trigger_based_notification_blocked_in_days(user)
  1173. return 0 if !user.preferences[:mail_delivery_failed]
  1174. return 0 if user.preferences[:mail_delivery_failed_data].blank?
  1175. # blocked for 60 full days; see #4459
  1176. remaining_days = (user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
  1177. return remaining_days if remaining_days.positive?
  1178. # cleanup user preferences
  1179. user.preferences[:mail_delivery_failed] = false
  1180. user.preferences[:mail_delivery_failed_data] = nil
  1181. user.save!
  1182. 0
  1183. end
  1184. end