123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class Whatsapp::Webhook::Message
- include Mixin::RequiredSubPaths
- attr_reader :data, :channel, :user, :ticket, :article
- def initialize(data:, channel:)
- @data = data
- @channel = channel
- end
- def process
- @user = create_or_update_user
- UserInfo.current_user_id = user.id
- @ticket = create_or_update_ticket
- @article = create_or_update_article
- schedule_reminder_job
- end
- private
- def attachment?
- false
- end
- def attachment
- raise NotImplementedError
- end
- def body
- raise NotImplementedError
- end
- def content_type
- raise NotImplementedError
- end
- def create_or_update_user
- User.by_mobile(number: user_info[:mobile]) || create_user
- end
- def create_or_update_ticket
- ticket = find_ticket
- return update_ticket(ticket:) if ticket.present?
- create_ticket
- end
- def create_ticket
- title = Translation.translate(Setting.get('locale_default') || 'en-us', __('%s via WhatsApp'), "#{profile_name} (#{@user.mobile})")
- Ticket.create!(
- group_id: @channel.group_id,
- title:,
- state_id: Ticket::State.find_by(default_create: true).id,
- priority_id: Ticket::Priority.find_by(default_create: true).id,
- customer_id: @user.id,
- preferences: {
- channel_id: @channel.id,
- channel_area: @channel.area,
- whatsapp: ticket_preferences,
- },
- )
- end
- def update_ticket(ticket:)
- new_state_id = ticket.state_id == default_create_ticket_state.id ? ticket.state_id : default_follow_up_ticket_state.id
- preferences = ticket.preferences
- preferences[:whatsapp] ||= {}
- preferences[:whatsapp][:timestamp_incoming] = @data[:entry].first[:changes].first[:value][:messages].first[:timestamp]
- ticket.update!(
- preferences:,
- state_id: new_state_id,
- )
- ticket
- end
- def find_ticket
- state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
- possible_tickets = Ticket.where(customer_id: @user.id).where.not(state_id: state_ids).reorder(:updated_at)
- possible_tickets.find_each.find { |possible_ticket| possible_ticket.preferences[:channel_id] == @channel.id }
- end
- def default_create_ticket_state
- Ticket::State.find_by(default_create: true)
- end
- def default_follow_up_ticket_state
- Ticket::State.find_by(default_follow_up: true)
- end
- def create_or_update_article
- # Editing messages results in being an unsupported type in the Cloud API. Nothing to do here!
- create_article
- end
- def create_article
- is_first_article = @ticket.articles.count.zero?
- article = Ticket::Article.create!(
- ticket_id: @ticket.id,
- type_id: Ticket::Article::Type.lookup(name: 'whatsapp message').id,
- sender_id: Ticket::Article::Sender.lookup(name: 'Customer').id,
- from: "#{profile_name} (#{@user.mobile})",
- to: "#{@channel.options[:name]} (#{@channel.options[:phone_number]})",
- message_id: article_preferences[:message_id],
- internal: false,
- body: body,
- content_type: content_type,
- preferences: {
- whatsapp: article_preferences,
- },
- )
- create_attachment(article: article) if attachment?
- create_welcome_article if is_first_article
- article
- end
- def create_attachment(article:)
- data, filename, mime_type = attachment
- Store.create!(
- object: 'Ticket::Article',
- o_id: article.id,
- data: data,
- filename: filename,
- preferences: {
- 'Mime-Type' => mime_type,
- },
- )
- rescue Whatsapp::Client::CloudAPIError
- preferences = article.preferences
- preferences[:whatsapp] ||= {}
- preferences[:whatsapp][:media_error] = true
- article.update!(preferences:)
- rescue Whatsapp::Incoming::Media::InvalidMediaTypeError => e
- article.update!(
- body: e.message,
- internal: true,
- )
- end
- def create_welcome_article
- return if @channel.options[:welcome].blank?
- translated_welcome_message = Translation.translate(user_locale, @channel.options[:welcome])
- Ticket::Article.create!(
- ticket_id: @ticket.id,
- type_id: Ticket::Article::Type.lookup(name: 'whatsapp message').id,
- sender_id: Ticket::Article::Sender.lookup(name: 'System').id,
- from: "#{@channel.options[:name]} (#{@channel.options[:phone_number]})",
- to: "#{profile_name} (#{@user.mobile})",
- subject: translated_welcome_message.truncate(100, omission: '…'),
- internal: false,
- body: translated_welcome_message,
- content_type: 'text/plain',
- )
- end
- def create_user
- user_data = user_info
- user_data[:active] = true
- user_data[:role_ids] = Role.signup_role_ids
- User.create(user_data)
- end
- def user_info
- firstname, lastname = User.name_guess(profile_name)
- # Fallback to profile name if no firstname or lastname is found
- if firstname.blank? || lastname.blank?
- firstname, lastname = profile_name.split(%r{\s|\.|,|,\s}, 2)
- end
- {
- firstname: firstname&.strip,
- lastname: lastname&.strip,
- mobile: "+#{phone}",
- login: phone,
- }
- end
- def user_locale
- @user.preferences[:locale] || Locale.default
- end
- def profile_name
- data[:entry].first[:changes].first[:value][:contacts].first[:profile][:name]
- end
- def phone
- data[:entry].first[:changes].first[:value][:messages].first[:from]
- end
- def ticket_preferences
- {
- from: {
- phone_number: phone,
- display_name: profile_name,
- },
- timestamp_incoming: @data[:entry].first[:changes].first[:value][:messages].first[:timestamp],
- }
- end
- def article_preferences
- {
- entry_id: @data[:entry].first[:id],
- message_id: @data[:entry].first[:changes].first[:value][:messages].first[:id],
- type: @data[:entry].first[:changes].first[:value][:messages].first[:type],
- }
- end
- def type
- raise NotImplementedError
- end
- def message
- @message ||= @data[:entry]
- .first[:changes]
- .first[:value][:messages]
- .first[type]
- end
- def schedule_reminder_job
- # Automatic reminders are an optional feature.
- return if !@channel.options[:reminder_active]
- # Do not schedule the job in case the service window has not been opened yet.
- timestamp_incoming = @ticket.preferences.dig(:whatsapp, :timestamp_incoming)
- return if timestamp_incoming.nil?
- cleanup_last_reminder_job
- # Calculate the end of the service window, based on the message timestamp.
- end_service_time = Time.zone.at(timestamp_incoming.to_i) + 24.hours
- return if end_service_time <= Time.zone.now
- # Set the reminder time to 1 hour before the service window closes and schedule a delayed job.
- reminder_time = end_service_time - 1.hour
- job = ScheduledWhatsappReminderJob.perform_at(reminder_time, @ticket, user_locale)
- # Remember reminder job information for subsequent runs.
- preferences = @ticket.preferences
- preferences[:whatsapp][:last_reminder_job_id] = job.provider_job_id
- ticket.update!(preferences:)
- end
- def cleanup_last_reminder_job
- last_job_id = @ticket.preferences.dig(:whatsapp, :last_reminder_job_id)
- return if last_job_id.nil?
- ::Delayed::Job.find_by(id: last_job_id)&.destroy!
- end
- end
|