message.rb 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class Whatsapp::Webhook::Message
  3. include Mixin::RequiredSubPaths
  4. attr_reader :data, :channel, :user, :ticket, :article
  5. def initialize(data:, channel:)
  6. @data = data
  7. @channel = channel
  8. end
  9. def process
  10. @user = create_or_update_user
  11. UserInfo.current_user_id = user.id
  12. @ticket = create_or_update_ticket
  13. @article = create_or_update_article
  14. schedule_reminder_job
  15. end
  16. private
  17. def attachment?
  18. false
  19. end
  20. def attachment
  21. raise NotImplementedError
  22. end
  23. def body
  24. raise NotImplementedError
  25. end
  26. def content_type
  27. raise NotImplementedError
  28. end
  29. def create_or_update_user
  30. User.by_mobile(number: user_info[:mobile]) || create_user
  31. end
  32. def create_or_update_ticket
  33. ticket = find_ticket
  34. return update_ticket(ticket:) if ticket.present?
  35. create_ticket
  36. end
  37. def create_ticket
  38. title = Translation.translate(Setting.get('locale_default') || 'en-us', __('%s via WhatsApp'), "#{profile_name} (#{@user.mobile})")
  39. Ticket.create!(
  40. group_id: @channel.group_id,
  41. title:,
  42. state_id: Ticket::State.find_by(default_create: true).id,
  43. priority_id: Ticket::Priority.find_by(default_create: true).id,
  44. customer_id: @user.id,
  45. preferences: {
  46. channel_id: @channel.id,
  47. channel_area: @channel.area,
  48. whatsapp: ticket_preferences,
  49. },
  50. )
  51. end
  52. def update_ticket(ticket:)
  53. new_state_id = ticket.state_id == default_create_ticket_state.id ? ticket.state_id : default_follow_up_ticket_state.id
  54. preferences = ticket.preferences
  55. preferences[:whatsapp] ||= {}
  56. preferences[:whatsapp][:timestamp_incoming] = @data[:entry].first[:changes].first[:value][:messages].first[:timestamp]
  57. ticket.update!(
  58. preferences:,
  59. state_id: new_state_id,
  60. )
  61. ticket
  62. end
  63. def find_ticket
  64. state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
  65. possible_tickets = Ticket.where(customer_id: @user.id).where.not(state_id: state_ids).reorder(:updated_at)
  66. possible_tickets.find_each.find { |possible_ticket| possible_ticket.preferences[:channel_id] == @channel.id }
  67. end
  68. def default_create_ticket_state
  69. Ticket::State.find_by(default_create: true)
  70. end
  71. def default_follow_up_ticket_state
  72. Ticket::State.find_by(default_follow_up: true)
  73. end
  74. def create_or_update_article
  75. # Editing messages results in being an unsupported type in the Cloud API. Nothing to do here!
  76. create_article
  77. end
  78. def create_article
  79. is_first_article = @ticket.articles.count.zero?
  80. article = Ticket::Article.create!(
  81. ticket_id: @ticket.id,
  82. type_id: Ticket::Article::Type.lookup(name: 'whatsapp message').id,
  83. sender_id: Ticket::Article::Sender.lookup(name: 'Customer').id,
  84. from: "#{profile_name} (#{@user.mobile})",
  85. to: "#{@channel.options[:name]} (#{@channel.options[:phone_number]})",
  86. message_id: article_preferences[:message_id],
  87. internal: false,
  88. body: body,
  89. content_type: content_type,
  90. preferences: {
  91. whatsapp: article_preferences,
  92. },
  93. )
  94. create_attachment(article: article) if attachment?
  95. create_welcome_article if is_first_article
  96. article
  97. end
  98. def create_attachment(article:)
  99. data, filename, mime_type = attachment
  100. Store.create!(
  101. object: 'Ticket::Article',
  102. o_id: article.id,
  103. data: data,
  104. filename: filename,
  105. preferences: {
  106. 'Mime-Type' => mime_type,
  107. },
  108. )
  109. rescue Whatsapp::Client::CloudAPIError
  110. preferences = article.preferences
  111. preferences[:whatsapp] ||= {}
  112. preferences[:whatsapp][:media_error] = true
  113. article.update!(preferences:)
  114. rescue Whatsapp::Incoming::Media::InvalidMediaTypeError => e
  115. article.update!(
  116. body: e.message,
  117. internal: true,
  118. )
  119. end
  120. def create_welcome_article
  121. return if @channel.options[:welcome].blank?
  122. translated_welcome_message = Translation.translate(user_locale, @channel.options[:welcome])
  123. Ticket::Article.create!(
  124. ticket_id: @ticket.id,
  125. type_id: Ticket::Article::Type.lookup(name: 'whatsapp message').id,
  126. sender_id: Ticket::Article::Sender.lookup(name: 'System').id,
  127. from: "#{@channel.options[:name]} (#{@channel.options[:phone_number]})",
  128. to: "#{profile_name} (#{@user.mobile})",
  129. subject: translated_welcome_message.truncate(100, omission: '…'),
  130. internal: false,
  131. body: translated_welcome_message,
  132. content_type: 'text/plain',
  133. )
  134. end
  135. def create_user
  136. user_data = user_info
  137. user_data[:active] = true
  138. user_data[:role_ids] = Role.signup_role_ids
  139. User.create(user_data)
  140. end
  141. def user_info
  142. firstname, lastname = User.name_guess(profile_name)
  143. # Fallback to profile name if no firstname or lastname is found
  144. if firstname.blank? || lastname.blank?
  145. firstname, lastname = profile_name.split(%r{\s|\.|,|,\s}, 2)
  146. end
  147. {
  148. firstname: firstname&.strip,
  149. lastname: lastname&.strip,
  150. mobile: "+#{phone}",
  151. login: phone,
  152. }
  153. end
  154. def user_locale
  155. @user.preferences[:locale] || Locale.default
  156. end
  157. def profile_name
  158. data[:entry].first[:changes].first[:value][:contacts].first[:profile][:name]
  159. end
  160. def phone
  161. data[:entry].first[:changes].first[:value][:messages].first[:from]
  162. end
  163. def ticket_preferences
  164. {
  165. from: {
  166. phone_number: phone,
  167. display_name: profile_name,
  168. },
  169. timestamp_incoming: @data[:entry].first[:changes].first[:value][:messages].first[:timestamp],
  170. }
  171. end
  172. def article_preferences
  173. {
  174. entry_id: @data[:entry].first[:id],
  175. message_id: @data[:entry].first[:changes].first[:value][:messages].first[:id],
  176. type: @data[:entry].first[:changes].first[:value][:messages].first[:type],
  177. }
  178. end
  179. def type
  180. raise NotImplementedError
  181. end
  182. def message
  183. @message ||= @data[:entry]
  184. .first[:changes]
  185. .first[:value][:messages]
  186. .first[type]
  187. end
  188. def schedule_reminder_job
  189. # Automatic reminders are an optional feature.
  190. return if !@channel.options[:reminder_active]
  191. # Do not schedule the job in case the service window has not been opened yet.
  192. timestamp_incoming = @ticket.preferences.dig(:whatsapp, :timestamp_incoming)
  193. return if timestamp_incoming.nil?
  194. cleanup_last_reminder_job
  195. # Calculate the end of the service window, based on the message timestamp.
  196. end_service_time = Time.zone.at(timestamp_incoming.to_i) + 24.hours
  197. return if end_service_time <= Time.zone.now
  198. # Set the reminder time to 1 hour before the service window closes and schedule a delayed job.
  199. reminder_time = end_service_time - 1.hour
  200. job = ScheduledWhatsappReminderJob.perform_at(reminder_time, @ticket, user_locale)
  201. # Remember reminder job information for subsequent runs.
  202. preferences = @ticket.preferences
  203. preferences[:whatsapp][:last_reminder_job_id] = job.provider_job_id
  204. ticket.update!(preferences:)
  205. end
  206. def cleanup_last_reminder_job
  207. last_job_id = @ticket.preferences.dig(:whatsapp, :last_reminder_job_id)
  208. return if last_job_id.nil?
  209. ::Delayed::Job.find_by(id: last_job_id)&.destroy!
  210. end
  211. end