slack.rb 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. class Transaction::Slack
  3. =begin
  4. backend = Transaction::Slack.new(
  5. object: 'Ticket',
  6. type: 'update',
  7. object_id: 123,
  8. interface_handle: 'application_server', # application_server|websocket|scheduler
  9. changes: {
  10. 'attribute1' => [before, now],
  11. 'attribute2' => [before, now],
  12. },
  13. created_at: Time.zone.now,
  14. user_id: 123,
  15. )
  16. backend.perform
  17. =end
  18. def initialize(item, params = {})
  19. @item = item
  20. @params = params
  21. end
  22. def perform
  23. # return if we run import mode
  24. return if Setting.get('import_mode')
  25. return if @item[:object] != 'Ticket'
  26. return if !Setting.get('slack_integration')
  27. config = Setting.get('slack_config')
  28. return if !config
  29. return if !config['items']
  30. ticket = Ticket.find_by(id: @item[:object_id])
  31. return if !ticket
  32. if @item[:article_id]
  33. article = Ticket::Article.find(@item[:article_id])
  34. # ignore notifications
  35. sender = Ticket::Article::Sender.lookup(id: article.sender_id)
  36. if sender&.name == 'System'
  37. return if @item[:changes].blank?
  38. article = nil
  39. end
  40. end
  41. # ignore if no changes has been done
  42. changes = human_changes(ticket)
  43. return if @item[:type] == 'update' && !article && changes.blank?
  44. # get user based notification template
  45. # if create, send create message / block update messages
  46. template = nil
  47. sent_value = nil
  48. case @item[:type]
  49. when 'create'
  50. template = 'ticket_create'
  51. when 'update'
  52. template = 'ticket_update'
  53. when 'reminder_reached'
  54. template = 'ticket_reminder_reached'
  55. sent_value = ticket.pending_time
  56. when 'escalation'
  57. template = 'ticket_escalation'
  58. sent_value = ticket.escalation_at
  59. when 'escalation_warning'
  60. template = 'ticket_escalation_warning'
  61. sent_value = ticket.escalation_at
  62. else
  63. raise "unknown type for notification #{@item[:type]}"
  64. end
  65. user = User.find(1)
  66. current_user = User.lookup(id: @item[:user_id])
  67. if !current_user
  68. current_user = User.lookup(id: 1)
  69. end
  70. result = NotificationFactory::Slack.template(
  71. template: template,
  72. locale: user.locale,
  73. timezone: Setting.get('timezone_default_sanitized'),
  74. objects: {
  75. ticket: ticket,
  76. article: article,
  77. current_user: current_user,
  78. changes: changes,
  79. },
  80. )
  81. # good, warning, danger
  82. color = '#000000'
  83. ticket_state_type = ticket.state.state_type.name
  84. if ticket.escalation_at && ticket.escalation_at < Time.zone.now
  85. color = '#f35912'
  86. elsif ticket_state_type == 'pending reminder'
  87. if ticket.pending_time && ticket.pending_time < Time.zone.now
  88. color = '#faab00'
  89. end
  90. elsif ticket_state_type.match?(%r{^(new|open)$})
  91. color = '#faab00'
  92. elsif ticket_state_type == 'closed'
  93. color = '#38ad69'
  94. end
  95. config['items'].each do |local_config|
  96. next if local_config['webhook'].blank?
  97. # check if reminder_reached/escalation/escalation_warning is already sent today
  98. md5_webhook = Digest::MD5.hexdigest(local_config['webhook'])
  99. cache_key = "slack::backend::#{@item[:type]}::#{ticket.id}::#{md5_webhook}"
  100. if sent_value
  101. value = Rails.cache.read(cache_key)
  102. if value == sent_value
  103. Rails.logger.debug { "did not send webhook, already sent (#{@item[:type]}/#{ticket.id}/#{local_config['webhook']})" }
  104. next
  105. end
  106. Rails.cache.write(
  107. cache_key,
  108. sent_value,
  109. {
  110. expires_in: 24.hours
  111. },
  112. )
  113. end
  114. # check action
  115. if local_config['types'].instance_of?(Array)
  116. hit = false
  117. local_config['types'].each do |type|
  118. next if type.to_s != @item[:type].to_s
  119. hit = true
  120. break
  121. end
  122. next if !hit
  123. elsif local_config['types']
  124. next if local_config['types'].to_s != @item[:type].to_s
  125. end
  126. # check group
  127. if local_config['group_ids'].instance_of?(Array)
  128. hit = false
  129. local_config['group_ids'].each do |group_id|
  130. next if group_id.to_s != ticket.group_id.to_s
  131. hit = true
  132. break
  133. end
  134. next if !hit
  135. elsif local_config['group_ids']
  136. next if local_config['group_ids'].to_s != ticket.group_id.to_s
  137. end
  138. logo_url = 'https://zammad.com/assets/images/logo-200x200.png'
  139. if local_config['logo_url'].present?
  140. logo_url = local_config['logo_url']
  141. end
  142. Rails.logger.debug { "sent webhook (#{@item[:type]}/#{ticket.id}/#{local_config['webhook']})" }
  143. require 'slack-notifier' # Only load this gem when it is really used.
  144. notifier = Slack::Notifier.new(
  145. local_config['webhook'],
  146. channel: local_config['channel'],
  147. username: local_config['username'],
  148. icon_url: logo_url,
  149. mrkdwn: true,
  150. http_client: Transaction::Slack::Client,
  151. )
  152. if local_config['expand']
  153. body = "#{result[:subject]}\n#{result[:body]}"
  154. result = notifier.ping body
  155. else
  156. attachment = {
  157. text: result[:body],
  158. mrkdwn_in: ['text'],
  159. color: color,
  160. }
  161. result = notifier.ping result[:subject],
  162. attachments: [attachment]
  163. end
  164. if !result.empty? && !result[0].success?
  165. if sent_value
  166. Rails.cache.delete(cache_key)
  167. end
  168. Rails.logger.error "Unable to post webhook: #{local_config['webhook']}: #{result.inspect}"
  169. next
  170. end
  171. Rails.logger.debug { "sent webhook (#{@item[:type]}/#{ticket.id}/#{local_config['webhook']})" }
  172. end
  173. end
  174. def human_changes(record)
  175. return {} if !@item[:changes]
  176. user = User.find(1)
  177. locale = user.preferences[:locale] || Setting.get('locale_default') || 'en-us'
  178. # only show allowed attributes
  179. attribute_list = ObjectManager::Object.new('Ticket').attributes(user).index_by { |item| item[:name] }
  180. # puts "AL #{attribute_list.inspect}"
  181. user_related_changes = {}
  182. @item[:changes].each do |key, value|
  183. # if no config exists, use all attributes
  184. # or if config exists, just use existing attributes for user
  185. if attribute_list.blank? || attribute_list[key.to_s]
  186. user_related_changes[key] = value
  187. end
  188. end
  189. changes = {}
  190. user_related_changes.each do |key, value|
  191. # get attribute name
  192. attribute_name = key.to_s
  193. object_manager_attribute = attribute_list[attribute_name]
  194. if attribute_name[-3, 3] == '_id'
  195. attribute_name = attribute_name[ 0, attribute_name.length - 3 ].to_s
  196. end
  197. # add item to changes hash
  198. if key.to_s == attribute_name
  199. changes[attribute_name] = value
  200. end
  201. # if changed item is an _id field/reference, look up the real values
  202. value_id = []
  203. value_str = [ value[0], value[1] ]
  204. if key.to_s[-3, 3] == '_id'
  205. value_id[0] = value[0]
  206. value_id[1] = value[1]
  207. if record.respond_to?(attribute_name) && record.send(attribute_name)
  208. relation_class = record.send(attribute_name).class
  209. if relation_class && value_id[0]
  210. relation_model = relation_class.lookup(id: value_id[0])
  211. if relation_model
  212. if relation_model['name']
  213. value_str[0] = relation_model['name']
  214. elsif relation_model.respond_to?(:fullname)
  215. value_str[0] = relation_model.send(:fullname)
  216. end
  217. end
  218. end
  219. if relation_class && value_id[1]
  220. relation_model = relation_class.lookup(id: value_id[1])
  221. if relation_model
  222. if relation_model['name']
  223. value_str[1] = relation_model['name']
  224. elsif relation_model.respond_to?(:fullname)
  225. value_str[1] = relation_model.send(:fullname)
  226. end
  227. end
  228. end
  229. end
  230. end
  231. # check if we have a dedicated display name for it
  232. display = attribute_name
  233. if object_manager_attribute && object_manager_attribute[:display]
  234. # delete old key
  235. changes.delete(display)
  236. # set new key
  237. display = object_manager_attribute[:display].to_s
  238. end
  239. changes[display] = if object_manager_attribute && object_manager_attribute[:translate]
  240. from = Translation.translate(locale, value_str[0])
  241. to = Translation.translate(locale, value_str[1])
  242. [from, to]
  243. else
  244. [value_str[0].to_s, value_str[1].to_s]
  245. end
  246. end
  247. changes
  248. end
  249. class Transaction::Slack::Client
  250. def self.post(uri, params = {})
  251. UserAgent.post(
  252. uri.to_s,
  253. params,
  254. {
  255. open_timeout: 4,
  256. read_timeout: 10,
  257. total_timeout: 20,
  258. log: {
  259. facility: 'slack_webhook',
  260. },
  261. verify_ssl: true,
  262. },
  263. )
  264. end
  265. end
  266. end