Browse Source

Fixes #5439 - Unsupported message type received by WhatsApp Business API is blocking the whole channel.

Florian Liebe 3 months ago
parent
commit
411d4da0f2

+ 4 - 0
i18n/zammad.pot

@@ -1456,6 +1456,10 @@ msgstr ""
 msgid "Any recipient"
 msgstr ""
 
+#: lib/whatsapp/webhook/concerns/handles_error.rb:49
+msgid "Apologies, we're unable to process this kind of message due to restrictions within WhatsApp Business."
+msgstr ""
+
 #: db/seeds/settings.rb:1426
 msgid "App ID"
 msgstr ""

+ 6 - 5
lib/whatsapp/client.rb

@@ -50,12 +50,13 @@ class Whatsapp::Client
 
       # https://developers.facebook.com/docs/graph-api/guides/error-handling
       recoverable_errors = [
-        130_472, # User's number is part of an experiment'
-        131_021, # Recipient cannot be sender'
-        131_026, # Message undeliverable'
+        130_472, # User's number is part of an experiment
+        131_021, # Recipient cannot be sender
+        131_026, # Message undeliverable
         131_047, # Re-engagement message
-        131_052, # Media download error'
-        131_053  # Media upload error'
+        131_051, # Unsupported message type
+        131_052, # Media download error
+        131_053, # Media upload error
       ]
       recoverable_errors.include?(original_error.code)
     end

+ 32 - 9
lib/whatsapp/webhook/concerns/handles_error.rb

@@ -17,20 +17,43 @@ module Whatsapp::Webhook
 
       Rails.logger.error "WhatsApp channel (#{@channel.options[:callback_url_uuid]}) - failed message: #{error[:title]} (#{error[:code]})"
 
-      recoverable_errors = [
-        130_472, # User's number is part of an experiment'
-        131_021, # Recipient cannot be sender'
-        131_026, # Message undeliverable'
-        131_047, # Re-engagement message
-        131_052, # Media download error'
-        131_053  # Media upload error'
-      ]
-      return if recoverable_errors.include?(error[:code])
+      return if recoverable_error?(error[:code])
+      return message_sender_error if sender_error?(error[:code])
 
       @channel.update!(
         status_out:   'error',
         last_log_out: "#{error[:title]} (#{error[:code]})",
       )
     end
+
+    def recoverable_error?(code)
+      [
+        130_472, # User's number is part of an experiment
+        131_021, # Recipient cannot be sender
+        131_026, # Message undeliverable
+        131_047, # Re-engagement message
+        131_052, # Media download error
+        131_053, # Media upload error
+      ].include?(code)
+    end
+
+    def sender_error?(code)
+      [
+        131_051, # Unsupported message type
+      ].include?(code)
+    end
+
+    def message_sender_error
+      body = Translation.translate(
+        Setting.get('locale_default') || 'en-us',
+        __("Apologies, we're unable to process this kind of message due to restrictions within WhatsApp Business.")
+      )
+
+      Whatsapp::Outgoing::Message::Text.new(
+        access_token:     @channel.options[:access_token],
+        phone_number_id:  @channel.options[:phone_number_id],
+        recipient_number: @data[:entry].first[:changes].first[:value][:messages].first[:from]
+      ).deliver(body:)
+    end
   end
 end

+ 160 - 0
spec/lib/whatsapp/webhook/payload/error_spec.rb

@@ -0,0 +1,160 @@
+# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe 'Whatsapp::Webhook::Payload::Error', :aggregate_failures, current_user_id: 1 do
+  let(:channel) { create(:whatsapp_channel) }
+  let(:wa_id)   { Faker::PhoneNumber.unique.cell_phone_in_e164.delete('+') }
+  let(:errors)  { {} }
+  let(:json) do
+    {
+      object: 'whatsapp_business_account',
+      entry:  [
+        {
+          id:      '222259550976437',
+          changes: [
+            {
+              value: {
+                messaging_product: 'whatsapp',
+                metadata:          {
+                  display_phone_number: channel.options[:phone_number],
+                  phone_number_id:      channel.options[:phone_number_id]
+                },
+                contacts:          [
+                  {
+                    profile: {
+                      name: Faker::Name.unique.name
+                    },
+                    wa_id:   wa_id
+                  }
+                ],
+                messages:          [
+                  {
+                    from:      wa_id,
+                    id:        'wamid.NDkxNTE1NTU1NTU5ODNBNTU3NkYyQTJCM0FGMUE1RjZECg==',
+                    timestamp: '1733484661',
+                    errors:    [
+                      error
+                    ],
+                    type:      'any'
+                  }
+                ]
+              },
+              field: 'messages'
+            }
+          ]
+        }
+      ]
+    }.to_json
+  end
+  let(:uuid) { channel.options[:callback_url_uuid] }
+  let(:signature) do
+    OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), channel.options[:app_secret], json)
+  end
+
+  context 'when error is initialized by sender' do
+
+    let(:error) do
+      {
+        code:       131_051,
+        title:      'Message type unknown',
+        message:    'Message type unknown',
+        error_data: {
+          details: 'Message type is currently not supported.'
+        }
+      }
+    end
+    let(:message) { instance_double(Whatsapp::Outgoing::Message::Text) }
+
+    before do
+      allow(Whatsapp::Outgoing::Message::Text)
+        .to receive(:new)
+        .and_return(message)
+      allow(message)
+        .to receive(:deliver)
+        .with(body: "Apologies, we're unable to process this kind of message due to restrictions within WhatsApp Business.")
+        .and_return(true)
+      allow(Rails.logger).to receive(:error)
+      begin
+        Whatsapp::Webhook::Payload.new(json: json, signature: signature, uuid: uuid).process
+      rescue Whatsapp::Webhook::Payload::ProcessableError
+        # noop
+      end
+    end
+
+    it 'does log error' do
+      expect(Rails.logger).to have_received(:error).once
+    end
+
+    it 'does inform sender' do
+      expect(message).to have_received(:deliver).once
+    end
+
+    it 'does not update channel status' do
+      expect(channel.reload.status_out).to be_nil
+      expect(channel.reload.last_log_out).to be_nil
+    end
+  end
+
+  context 'when error is recoverable' do
+    let(:error) do
+      {
+        code:       131_026,
+        title:      'Message undeliverable',
+        message:    'Message undeliverable',
+        error_data: {
+          details: 'Message could not be delivered.'
+        }
+      }
+    end
+
+    before do
+      allow(Rails.logger).to receive(:error)
+      begin
+        Whatsapp::Webhook::Payload.new(json: json, signature: signature, uuid: uuid).process
+      rescue Whatsapp::Webhook::Payload::ProcessableError
+        # noop
+      end
+    end
+
+    it 'does log error' do
+      expect(Rails.logger).to have_received(:error).once
+    end
+
+    it 'does not update channel status' do
+      expect(channel.reload.status_out).to be_nil
+      expect(channel.reload.last_log_out).to be_nil
+    end
+  end
+
+  context 'when error is not recoverable' do
+    let(:error) do
+      {
+        code:       131_056,
+        title:      'Pair rate limit hit',
+        message:    'Pair rate limit hit',
+        error_data: {
+          details: 'Too many messages sent to this user.'
+        }
+      }
+    end
+
+    before do
+      allow(Rails.logger).to receive(:error)
+      begin
+        Whatsapp::Webhook::Payload.new(json: json, signature: signature, uuid: uuid).process
+      rescue Whatsapp::Webhook::Payload::ProcessableError
+        # noop
+      end
+    end
+
+    it 'does log error' do
+      expect(Rails.logger).to have_received(:error).once
+    end
+
+    it 'does update channel status' do
+      expect(channel.reload.status_out).to eq('error')
+      expect(channel.reload.last_log_out).to eq('Pair rate limit hit (131056)')
+    end
+  end
+end

+ 5 - 5
spec/lib/whatsapp/webhook/payload_spec.rb

@@ -133,9 +133,9 @@ RSpec.describe Whatsapp::Webhook::Payload, :aggregate_failures, current_user_id:
                   },
                   errors:    [
                     {
-                      code:    131_051,
-                      details: 'Message type is not currently supported',
-                      title:   'Unsupported message type'
+                      code:    0,
+                      details: 'Unable to authenticate the app user',
+                      title:   'AuthException'
                     }
                   ],
                   type:      type
@@ -161,7 +161,7 @@ RSpec.describe Whatsapp::Webhook::Payload, :aggregate_failures, current_user_id:
         end
 
         expect(Rails.logger).to have_received(:error)
-          .with("WhatsApp channel (#{channel.options[:callback_url_uuid]}) - failed message: Unsupported message type (131051)")
+          .with("WhatsApp channel (#{channel.options[:callback_url_uuid]}) - failed message: AuthException (0)")
       end
 
       it 'updates the channel status' do
@@ -173,7 +173,7 @@ RSpec.describe Whatsapp::Webhook::Payload, :aggregate_failures, current_user_id:
 
         expect(channel.reload).to have_attributes(
           status_out:   'error',
-          last_log_out: 'Unsupported message type (131051)',
+          last_log_out: 'AuthException (0)',
         )
       end
     end