From 0ec5c467582e485ee0afbba23bd930a1558ea847 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Sat, 6 May 2023 07:43:30 +0200 Subject: [PATCH 01/57] Add WhatsAppAdapter::Outbound::Template class --- app/adapters/whats_app_adapter/outbound.rb | 26 ++++++++++++++++--- .../whats_app_adapter/outbound/template.rb | 24 +++++++++++++++++ .../whats_app/webhook_controller.rb | 5 ++++ app/views/dashboard/index.html.erb | 3 ++- config/routes.rb | 1 + 5 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 app/adapters/whats_app_adapter/outbound/template.rb diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index 44f560c90..d2fd26942 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -78,9 +78,29 @@ def freeform_message_permitted?(recipient) def send_message_template(recipient, message) recipient.update(whats_app_template_message_sent_at: Time.current) - text = I18n.t("adapter.whats_app.request_template.new_request_#{time_of_day}_#{rand(1..3)}", first_name: recipient.first_name, - request_title: message.request.title) - WhatsAppAdapter::Outbound::Text.perform_later(recipient: recipient, text: text) + WhatsAppAdapter::Outbound::Template.perform_later(payload: { + to: recipient.whats_app_phone_number.split('+').last, + type: 'template', + template: { + namespace: ENV.fetch('WHATS_APP_MESSAGE_TEMPLATE_NAMESPACE', ''), + language: { + policy: 'deterministic', + code: 'de' + }, + name: "new_request_#{time_of_day}_#{rand(1..3)}", + components: [{ + type: 'body', + parameters: [{ + type: 'text', + text: recipient.first_name + }, + { + type: 'text', + text: message.request.title + }] + }] + } + }) end def send_message(recipient, message) diff --git a/app/adapters/whats_app_adapter/outbound/template.rb b/app/adapters/whats_app_adapter/outbound/template.rb new file mode 100644 index 000000000..8d80dd9b3 --- /dev/null +++ b/app/adapters/whats_app_adapter/outbound/template.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module WhatsAppAdapter + class Outbound + class Template < ApplicationJob + queue_as :default + + def perform(payload:) + url = URI.parse('https://hub.360dialog.com/v1/messages') + request = Net::HTTP::Post.new(url.to_s, { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: "Bearer #{ENV.fetch('360_DIALOG_ACCESS_TOKEN', '')}" + }) + + request.body = payload.to_json + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| + http.request(request) + end + response.value # may raise exception + end + end + end +end diff --git a/app/controllers/whats_app/webhook_controller.rb b/app/controllers/whats_app/webhook_controller.rb index 40700ee10..1a1b2f7af 100644 --- a/app/controllers/whats_app/webhook_controller.rb +++ b/app/controllers/whats_app/webhook_controller.rb @@ -55,6 +55,11 @@ def status ErrorNotifier.report(exception, context: { message_sid: status_params['MessageSid'] }) end + def three_sixty_dialog_status + Rails.logger.debug params + head :ok + end + private def message_params diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 1029aded4..5857f3168 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -3,7 +3,8 @@ <%= c 'heading' do %> <%= t "dashboard.heading#{time_based_heading}", first_name: current_user.first_name %> <% end %> - <%= c 'new_request_link' %> + <%= c 'button', link: "https://hub.360dialog.com/dashboard/app/E7qlilPA/permissions", label: 'Connect to 360dialog' %> + <%# <%= c 'new_request_link' %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index 1410bf566..98cb3b734 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -48,6 +48,7 @@ post '/webhook', to: 'webhook#message' post '/errors', to: 'webhook#errors' post '/status', to: 'webhook#status' + post '/360dialog-status', to: 'webhook#three_sixty_dialog_status' end telegram_webhook Telegram::WebhookController From 9b29a35cde0b4ab6fd79e288f2fc66c98e00c143 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 10 May 2023 09:37:08 +0200 Subject: [PATCH 02/57] Start working on consumption of redirect after onboarding --- .../whats_app/onboarding_controller.rb | 13 +++++++++++ app/jobs/whats_app_adapter/create_api_key.rb | 22 +++++++++++++++++++ app/models/setting.rb | 2 ++ app/views/dashboard/index.html.erb | 6 ++++- .../whats_app/onboarding/success.html.erb | 5 +++++ config/routes.rb | 3 ++- 6 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 app/controllers/whats_app/onboarding_controller.rb create mode 100644 app/jobs/whats_app_adapter/create_api_key.rb create mode 100644 app/views/whats_app/onboarding/success.html.erb diff --git a/app/controllers/whats_app/onboarding_controller.rb b/app/controllers/whats_app/onboarding_controller.rb new file mode 100644 index 000000000..7e1bcaf9b --- /dev/null +++ b/app/controllers/whats_app/onboarding_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module WhatsApp + class OnboardingController < ApplicationController + def success + Rails.logger.debug params + channel_ids = params[:channels].split(',') + channel_ids.each do |channel_id| + WhatsAppAdapter::CreateApiKey.perform_later(channel_id: channel_id) + end + end + end +end diff --git a/app/jobs/whats_app_adapter/create_api_key.rb b/app/jobs/whats_app_adapter/create_api_key.rb new file mode 100644 index 000000000..e30d221a3 --- /dev/null +++ b/app/jobs/whats_app_adapter/create_api_key.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'net/http' + +module WhatsAppAdapter + class CreateApiKey + def perform(channel_id:) + url = URI.parse(" https://hub.360dialog.io/api/v2/partners/#{ENV.fetch('360_DIALOG_PARTNER_ID', '')}/channels/#{channel_id}/api_keys") + headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: "Bearer #{ENV.fetch('360_DIALOG_ACCESS_TOKEN', '')}" + } + request = Net::HTTP::Post.new(url.to_s, headers) + response = Net::HTTP.start(url.host, url.port) do |http| + http.request(request) + end + api_key = response.body['api_key'] + Setting.three_sixty_dialog_api_key = api_key + end + end +end diff --git a/app/models/setting.rb b/app/models/setting.rb index 06fcddd97..af23d42ce 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -72,6 +72,8 @@ def self.onboarding_hero=(blob) field :twilio_api_key_secret, readonly: true, default: ENV.fetch('TWILIO_API_KEY_SECRET', nil) field :whats_app_server_phone_number, readonly: true, default: ENV.fetch('WHATS_APP_SERVER_PHONE_NUMBER', nil) + field :three_sixty_dialog_api_key + field :inbound_email_password, readonly: true, default: ENV.fetch('RAILS_INBOUND_EMAIL_PASSWORD', nil) field :email_from_address, readonly: true, default: ENV['EMAIL_FROM_ADDRESS'] || 'redaktion@localhost' field :postmark_api_token, readonly: true, default: ENV.fetch('POSTMARK_API_TOKEN', nil) diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 5857f3168..8c9e76a79 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -3,7 +3,11 @@ <%= c 'heading' do %> <%= t "dashboard.heading#{time_based_heading}", first_name: current_user.first_name %> <% end %> - <%= c 'button', link: "https://hub.360dialog.com/dashboard/app/E7qlilPA/permissions", label: 'Connect to 360dialog' %> + <%= c 'button', + link: "https://hub.360dialog.com/dashboard/app/#{ENV.fetch('360_DIALOG_PARTNER_ID', '')}/permissions?email=#{current_user.email}&name=#{current_user.name}&redirect_url=#{CGI.escape(whats_app_onboarding_successful_url)}", + label: 'Connect to 360dialog', + target: "_blank" + %> <%# <%= c 'new_request_link' %> <% end %> diff --git a/app/views/whats_app/onboarding/success.html.erb b/app/views/whats_app/onboarding/success.html.erb new file mode 100644 index 000000000..85a5dc30c --- /dev/null +++ b/app/views/whats_app/onboarding/success.html.erb @@ -0,0 +1,5 @@ +<%= c 'onboarding_response', + style: :success, + heading: Setting.onboarding_success_heading, project_name: Setting.project_name, + text: Setting.onboarding_success_text +%> diff --git a/config/routes.rb b/config/routes.rb index 98cb3b734..f98f2b418 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -48,7 +48,8 @@ post '/webhook', to: 'webhook#message' post '/errors', to: 'webhook#errors' post '/status', to: 'webhook#status' - post '/360dialog-status', to: 'webhook#three_sixty_dialog_status' + post '/three-sixty-dialog-status', to: 'webhook#three_sixty_dialog_status' + get '/onboarding-successful', to: 'onboarding#success' end telegram_webhook Telegram::WebhookController From 2cd98ef3355e0b5529470bda8f03c2dd504ea361 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 11 May 2023 07:44:09 +0200 Subject: [PATCH 03/57] Update create api key flow --- app/controllers/whats_app/onboarding_controller.rb | 2 +- app/jobs/whats_app_adapter/create_api_key.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/whats_app/onboarding_controller.rb b/app/controllers/whats_app/onboarding_controller.rb index 7e1bcaf9b..e9838e8fa 100644 --- a/app/controllers/whats_app/onboarding_controller.rb +++ b/app/controllers/whats_app/onboarding_controller.rb @@ -4,7 +4,7 @@ module WhatsApp class OnboardingController < ApplicationController def success Rails.logger.debug params - channel_ids = params[:channels].split(',') + channel_ids = params[:channels].split('[').last.split(']').last.split(',') channel_ids.each do |channel_id| WhatsAppAdapter::CreateApiKey.perform_later(channel_id: channel_id) end diff --git a/app/jobs/whats_app_adapter/create_api_key.rb b/app/jobs/whats_app_adapter/create_api_key.rb index e30d221a3..b7641dc7a 100644 --- a/app/jobs/whats_app_adapter/create_api_key.rb +++ b/app/jobs/whats_app_adapter/create_api_key.rb @@ -3,16 +3,16 @@ require 'net/http' module WhatsAppAdapter - class CreateApiKey + class CreateApiKey < ApplicationJob def perform(channel_id:) - url = URI.parse(" https://hub.360dialog.io/api/v2/partners/#{ENV.fetch('360_DIALOG_PARTNER_ID', '')}/channels/#{channel_id}/api_keys") + url = URI.parse("https://hub.360dialog.io/api/v2/partners/#{ENV.fetch('360_DIALOG_PARTNER_ID', '')}/channels/#{channel_id}/api_keys") headers = { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: "Bearer #{ENV.fetch('360_DIALOG_ACCESS_TOKEN', '')}" } request = Net::HTTP::Post.new(url.to_s, headers) - response = Net::HTTP.start(url.host, url.port) do |http| + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| http.request(request) end api_key = response.body['api_key'] From 64f344c9a35b9f091b649a22ea6cfe24c3741a74 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Sat, 3 Jun 2023 09:42:10 +0200 Subject: [PATCH 04/57] Clean up env variables, lint --- .env.template | 8 +++ app/adapters/whats_app_adapter/outbound.rb | 53 ++++++++++--------- .../whats_app_adapter/outbound/template.rb | 24 --------- .../outbound/three_sixty_dialog_text.rb | 21 ++++++++ app/jobs/whats_app_adapter/create_api_key.rb | 33 ++++++++++-- app/models/setting.rb | 22 ++++++++ 6 files changed, 109 insertions(+), 52 deletions(-) delete mode 100644 app/adapters/whats_app_adapter/outbound/template.rb create mode 100644 app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb diff --git a/.env.template b/.env.template index e6a5c3264..98f560594 100644 --- a/.env.template +++ b/.env.template @@ -24,3 +24,11 @@ THREEMARB_PRIVATE= # POSTMARK_TRANSACTIONAL_STREAM=outbound # DOCKER_IMAGE_TAG= # SENTRY_DSN= +# +# 360_DIALOG_ACCESS_TOKEN= +# 360_DIALOG_PARTNER_ID= +# 360_DIALOG_PARTNER_USERNAME= +# 360_DIALOG_PARTNER_PASSWORD= +# 360_DIALOG_WHATS_APP_MESSAGE_TEMPLATE_NAMESPACE= +# 360_DIALOG_WHATS_APP_REST_API_ENDPOINT= +# 360_DIALOG_PARTNER_REST_API_ENDPOINT= \ No newline at end of file diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index d2fd26942..6f0a69659 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -77,30 +77,7 @@ def freeform_message_permitted?(recipient) def send_message_template(recipient, message) recipient.update(whats_app_template_message_sent_at: Time.current) - - WhatsAppAdapter::Outbound::Template.perform_later(payload: { - to: recipient.whats_app_phone_number.split('+').last, - type: 'template', - template: { - namespace: ENV.fetch('WHATS_APP_MESSAGE_TEMPLATE_NAMESPACE', ''), - language: { - policy: 'deterministic', - code: 'de' - }, - name: "new_request_#{time_of_day}_#{rand(1..3)}", - components: [{ - type: 'body', - parameters: [{ - type: 'text', - text: recipient.first_name - }, - { - type: 'text', - text: message.request.title - }] - }] - } - }) + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: template_payload(recipient, message)) end def send_message(recipient, message) @@ -114,6 +91,34 @@ def send_message(recipient, message) end end end + + def template_payload(recipient, message) + template_namespace = Setting.three_sixty_dialog[:whats_app][:template_namespace] + { + recipient_type: 'individual', + to: recipient.whats_app_phone_number.split('+').last, + type: 'template', + template: { + namespace: template_namespace, + language: { + policy: 'deterministic', + code: 'de' + }, + name: "new_request_#{time_of_day}_#{rand(1..3)}", + components: [{ + type: 'body', + parameters: [{ + type: 'text', + text: recipient.first_name + }, + { + type: 'text', + text: message.request.title + }] + }] + } + } + end end end end diff --git a/app/adapters/whats_app_adapter/outbound/template.rb b/app/adapters/whats_app_adapter/outbound/template.rb deleted file mode 100644 index 8d80dd9b3..000000000 --- a/app/adapters/whats_app_adapter/outbound/template.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module WhatsAppAdapter - class Outbound - class Template < ApplicationJob - queue_as :default - - def perform(payload:) - url = URI.parse('https://hub.360dialog.com/v1/messages') - request = Net::HTTP::Post.new(url.to_s, { - Accept: 'application/json', - 'Content-Type': 'application/json', - Authorization: "Bearer #{ENV.fetch('360_DIALOG_ACCESS_TOKEN', '')}" - }) - - request.body = payload.to_json - response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| - http.request(request) - end - response.value # may raise exception - end - end - end -end diff --git a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb new file mode 100644 index 000000000..939d3cf43 --- /dev/null +++ b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module WhatsAppAdapter + class Outbound + class ThreeSixtyDialogText < ApplicationJob + queue_as :default + + def perform(payload:) + url = URI.parse("#{Setting.three_sixty_dialog[:whats_app][:rest_api_endpoint]}/messages") + headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_api_key, 'Content-Type' => 'application/json' } + request = Net::HTTP::Post.new(url.to_s, headers) + + request.body = payload.to_json + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| + http.request(request) + end + response.value # may raise exception + end + end + end +end diff --git a/app/jobs/whats_app_adapter/create_api_key.rb b/app/jobs/whats_app_adapter/create_api_key.rb index b7641dc7a..83cf815c4 100644 --- a/app/jobs/whats_app_adapter/create_api_key.rb +++ b/app/jobs/whats_app_adapter/create_api_key.rb @@ -3,20 +3,45 @@ require 'net/http' module WhatsAppAdapter - class CreateApiKey < ApplicationJob + class CreateApiKey < ApplicationJob def perform(channel_id:) - url = URI.parse("https://hub.360dialog.io/api/v2/partners/#{ENV.fetch('360_DIALOG_PARTNER_ID', '')}/channels/#{channel_id}/api_keys") + token = Setting.find_by(var: 'three_sixty_dialog_partner_token') + fetch_token unless token&.value && token.updated_at > 24.hours.ago + base_uri = Setting.three_sixty_dialog[:partner][:rest_api_endpoint] + partner_id = Setting.three_sixty_dialog[:partner][:id] + url = URI.parse( + "#{base_uri}/partners/#{partner_id}/channels/#{channel_id}/api_keys" + ) headers = { Accept: 'application/json', 'Content-Type': 'application/json', - Authorization: "Bearer #{ENV.fetch('360_DIALOG_ACCESS_TOKEN', '')}" + Authorization: "Bearer #{Setting.three_sixty_dialog_partner_token}" } request = Net::HTTP::Post.new(url.to_s, headers) response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| http.request(request) end - api_key = response.body['api_key'] + api_key = JSON.parse(response.body)['api_key'] Setting.three_sixty_dialog_api_key = api_key end + + private + + def fetch_token + url = URI.parse("#{Setting.three_sixty_dialog_partner_rest_api_endpoint}/token") + headers = { + 'Content-Type': 'application/json' + } + request = Net::HTTP::Post.new(url.to_s, headers) + request.body = { + username: Setting.three_sixty_dialog_partner_username, + password: Setting.three_sixty_dialog_partner_password + }.to_json + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| + http.request(request) + end + token = JSON.parse(response.body)['access_token'] + Setting.three_sixty_dialog_partner_token = token + end end end diff --git a/app/models/setting.rb b/app/models/setting.rb index af23d42ce..bcfe9bc41 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -72,8 +72,30 @@ def self.onboarding_hero=(blob) field :twilio_api_key_secret, readonly: true, default: ENV.fetch('TWILIO_API_KEY_SECRET', nil) field :whats_app_server_phone_number, readonly: true, default: ENV.fetch('WHATS_APP_SERVER_PHONE_NUMBER', nil) + field :three_sixty_dialog_partner_token + field :three_sixty_dialog, type: :hash, readonly: true, default: { + partner: { + id: ENV.fetch('360_DIALOG_PARTNER_ID', nil), + username: ENV.fetch('360_DIALOG_PARTNER_USERNAME', nil), + password: ENV.fetch('360_DIALOG_PARTNER_PASSWORD', nil), + rest_api_endpoint: ENV.fetch('360_DIALOG_PARTNER_REST_API_ENDPOINT', 'https://stoplight.io/mocks/360dialog/360dialog-partner-api/24588693') + }, + whats_app: { + rest_api_endpoint: ENV.fetch('360_DIALOG_WHATS_APP_REST_API_ENDPOINT', 'https://waba-sandbox.360dialog.io'), + template_namespace: ENV.fetch('360_DIALOG_WHATS_APP_MESSAGE_TEMPLATE_NAMESPACE', nil) + } + } + # field :three_sixty_dialog_partner_id, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_ID', nil) + # field :three_sixty_dialog_partner_username, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_USERNAME', nil) + # field :three_sixty_dialog_partner_password, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_PASSWORD', nil) + # field :three_sixty_dialog_partner_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_REST_API_ENDPOINT', 'https://stoplight.io/mocks/360dialog/360dialog-partner-api/24588693') + field :three_sixty_dialog_api_key + # field :three_sixty_dialog_whats_app_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_WHATS_APP_REST_API_ENDPOINT', 'https://waba-sandbox.360dialog.io') + # field :three_sixty_dialog_whats_app_template_namespace, readonly: true, + # default: ENV.fetch('360_DIALOG_WHATS_APP_MESSAGE_TEMPLATE_NAMESPACE', nil) + field :inbound_email_password, readonly: true, default: ENV.fetch('RAILS_INBOUND_EMAIL_PASSWORD', nil) field :email_from_address, readonly: true, default: ENV['EMAIL_FROM_ADDRESS'] || 'redaktion@localhost' field :postmark_api_token, readonly: true, default: ENV.fetch('POSTMARK_API_TOKEN', nil) From 0b4f99c4c581820be90f09d60c6c1dbe80e0b06b Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 1 Aug 2023 17:30:35 +0200 Subject: [PATCH 05/57] Update Setting names --- app/adapters/whats_app_adapter/outbound.rb | 4 +-- .../outbound/three_sixty_dialog_text.rb | 2 +- app/jobs/whats_app_adapter/create_api_key.rb | 25 +++++++++++++---- app/models/setting.rb | 28 ++++++------------- 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index f8361222f..8f9fbf668 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -93,7 +93,7 @@ def send_message(recipient, message) end def template_payload(recipient, message) - template_namespace = Setting.three_sixty_dialog[:whats_app][:template_namespace] + template_namespace = Setting.three_sixty_dialog_whats_app_template_namespace { recipient_type: 'individual', to: recipient.whats_app_phone_number.split('+').last, @@ -104,7 +104,7 @@ def template_payload(recipient, message) policy: 'deterministic', code: 'de' }, - name: "new_request_#{time_of_day}_#{rand(1..3)}", + name: 'new_request_morning_1', # TODO: Use dynamic template name after WhatsAppAdapter::CreateTemplate works components: [{ type: 'body', parameters: [{ diff --git a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb index 939d3cf43..7b7d6ad59 100644 --- a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb +++ b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb @@ -6,7 +6,7 @@ class ThreeSixtyDialogText < ApplicationJob queue_as :default def perform(payload:) - url = URI.parse("#{Setting.three_sixty_dialog[:whats_app][:rest_api_endpoint]}/messages") + url = URI.parse("#{Setting.three_sixty_dialog_whats_app_rest_api_endpoint}/messages") headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_api_key, 'Content-Type' => 'application/json' } request = Net::HTTP::Post.new(url.to_s, headers) diff --git a/app/jobs/whats_app_adapter/create_api_key.rb b/app/jobs/whats_app_adapter/create_api_key.rb index 83cf815c4..c110d0c58 100644 --- a/app/jobs/whats_app_adapter/create_api_key.rb +++ b/app/jobs/whats_app_adapter/create_api_key.rb @@ -5,10 +5,13 @@ module WhatsAppAdapter class CreateApiKey < ApplicationJob def perform(channel_id:) + return if Setting.three_sixty_dialog_api_key.present? + + @base_uri = Setting.three_sixty_dialog_partner_rest_api_endpoint + token = Setting.find_by(var: 'three_sixty_dialog_partner_token') fetch_token unless token&.value && token.updated_at > 24.hours.ago - base_uri = Setting.three_sixty_dialog[:partner][:rest_api_endpoint] - partner_id = Setting.three_sixty_dialog[:partner][:id] + partner_id = Setting.three_sixty_dialog_partner_id url = URI.parse( "#{base_uri}/partners/#{partner_id}/channels/#{channel_id}/api_keys" ) @@ -21,14 +24,15 @@ def perform(channel_id:) response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| http.request(request) end - api_key = JSON.parse(response.body)['api_key'] - Setting.three_sixty_dialog_api_key = api_key + handle_response(response) end private + attr_reader :base_uri + def fetch_token - url = URI.parse("#{Setting.three_sixty_dialog_partner_rest_api_endpoint}/token") + url = URI.parse("#{base_uri}/token") headers = { 'Content-Type': 'application/json' } @@ -43,5 +47,16 @@ def fetch_token token = JSON.parse(response.body)['access_token'] Setting.three_sixty_dialog_partner_token = token end + + def handle_response(response) + case response.code.to_i + when 200 + api_key = JSON.parse(response.body)['api_key'] + Setting.three_sixty_dialog_api_key = api_key + when 400..599 + exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) + ErrorNotifier.report(exception) + end + end end end diff --git a/app/models/setting.rb b/app/models/setting.rb index bcfe9bc41..10ca5af9a 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -73,28 +73,18 @@ def self.onboarding_hero=(blob) field :whats_app_server_phone_number, readonly: true, default: ENV.fetch('WHATS_APP_SERVER_PHONE_NUMBER', nil) field :three_sixty_dialog_partner_token - field :three_sixty_dialog, type: :hash, readonly: true, default: { - partner: { - id: ENV.fetch('360_DIALOG_PARTNER_ID', nil), - username: ENV.fetch('360_DIALOG_PARTNER_USERNAME', nil), - password: ENV.fetch('360_DIALOG_PARTNER_PASSWORD', nil), - rest_api_endpoint: ENV.fetch('360_DIALOG_PARTNER_REST_API_ENDPOINT', 'https://stoplight.io/mocks/360dialog/360dialog-partner-api/24588693') - }, - whats_app: { - rest_api_endpoint: ENV.fetch('360_DIALOG_WHATS_APP_REST_API_ENDPOINT', 'https://waba-sandbox.360dialog.io'), - template_namespace: ENV.fetch('360_DIALOG_WHATS_APP_MESSAGE_TEMPLATE_NAMESPACE', nil) - } - } - # field :three_sixty_dialog_partner_id, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_ID', nil) - # field :three_sixty_dialog_partner_username, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_USERNAME', nil) - # field :three_sixty_dialog_partner_password, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_PASSWORD', nil) - # field :three_sixty_dialog_partner_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_REST_API_ENDPOINT', 'https://stoplight.io/mocks/360dialog/360dialog-partner-api/24588693') + field :three_sixty_dialog_partner_id, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_ID', nil) + field :three_sixty_dialog_partner_username, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_USERNAME', nil) + field :three_sixty_dialog_partner_password, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_PASSWORD', nil) + field :three_sixty_dialog_partner_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_REST_API_ENDPOINT', 'https://stoplight.io/mocks/360dialog/360dialog-partner-api/24588693') field :three_sixty_dialog_api_key + field :three_sixty_dialog_client_id, readonly: true, default: ENV.fetch('360_DIALOG_CLIENT_ID', nil) + field :three_sixty_dialog_client_waba_account_id - # field :three_sixty_dialog_whats_app_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_WHATS_APP_REST_API_ENDPOINT', 'https://waba-sandbox.360dialog.io') - # field :three_sixty_dialog_whats_app_template_namespace, readonly: true, - # default: ENV.fetch('360_DIALOG_WHATS_APP_MESSAGE_TEMPLATE_NAMESPACE', nil) + field :three_sixty_dialog_whats_app_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_WHATS_APP_REST_API_ENDPOINT', 'https://waba-sandbox.360dialog.io') + field :three_sixty_dialog_whats_app_template_namespace, readonly: true, + default: ENV.fetch('360_DIALOG_WHATS_APP_MESSAGE_TEMPLATE_NAMESPACE', nil) field :inbound_email_password, readonly: true, default: ENV.fetch('RAILS_INBOUND_EMAIL_PASSWORD', nil) field :email_from_address, readonly: true, default: ENV['EMAIL_FROM_ADDRESS'] || 'redaktion@localhost' From 4dfa0d6c326d2361c94eacdbcac66f86d79cedda Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 1 Aug 2023 17:31:53 +0200 Subject: [PATCH 06/57] Add rake task/jobs for initial config --- .../three_sixty_dialog_error.rb | 9 ++ app/jobs/whats_app_adapter/create_template.rb | 124 ++++++++++++++++++ app/jobs/whats_app_adapter/set_webhook_url.rb | 32 +++++ lib/tasks/whats_app/create_templates.rake | 12 ++ 4 files changed, 177 insertions(+) create mode 100644 app/adapters/whats_app_adapter/three_sixty_dialog_error.rb create mode 100644 app/jobs/whats_app_adapter/create_template.rb create mode 100644 app/jobs/whats_app_adapter/set_webhook_url.rb create mode 100644 lib/tasks/whats_app/create_templates.rake diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_error.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_error.rb new file mode 100644 index 000000000..d5a7c0800 --- /dev/null +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_error.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module WhatsAppAdapter + class ThreeSixtyDialogError < StandardError + def initialize(error_code:, message:) + super("Error occurred for WhatsApp with error code: #{error_code} with message: #{message}") + end + end +end diff --git a/app/jobs/whats_app_adapter/create_template.rb b/app/jobs/whats_app_adapter/create_template.rb new file mode 100644 index 000000000..9d5bb23b7 --- /dev/null +++ b/app/jobs/whats_app_adapter/create_template.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'net/http' + +module WhatsAppAdapter + class CreateTemplate < ApplicationJob + def perform(template_name:, template_text:) + @base_uri = Setting.three_sixty_dialog_partner_rest_api_endpoint + @partner_id = Setting.three_sixty_dialog_partner_id + @template_name = template_name + @template_text = template_text + + token = Setting.find_by(var: 'three_sixty_dialog_partner_token') + fetch_token unless token&.value && token.updated_at > 24.hours.ago + + waba_account_id = Setting.three_sixty_dialog_client_waba_account_id + waba_account_id = fetch_waba_account_id if waba_account_id.blank? + + url = URI.parse( + "#{base_uri}/partners/#{partner_id}/waba_accounts/#{waba_account_id}/waba_templates" + ) + headers = set_headers + + request = Net::HTTP::Post.new(url.to_s, headers) + request.body = template_payload.to_json + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| + http.request(request) + end + handle_response(response) + end + + attr_reader :base_uri, :partner_id, :template_name, :template_text + + private + + def set_headers + { + 'D360-API-KEY': Setting.three_sixty_dialog_api_key, + 'Content-Type': 'application/json', + Authorization: "Bearer #{Setting.three_sixty_dialog_partner_token}" + } + end + + def fetch_token + url = URI.parse("#{base_uri}/token") + headers = { 'Content-Type': 'application/json' } + request = Net::HTTP::Post.new(url.to_s, headers) + request.body = { + username: Setting.three_sixty_dialog_partner_username, + password: Setting.three_sixty_dialog_partner_password + }.to_json + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| + http.request(request) + end + token = JSON.parse(response.body)['access_token'] + Setting.three_sixty_dialog_partner_token = token + end + + def fetch_waba_account_id + url = URI.parse("#{base_uri}/partners/#{partner_id}/channels") + headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: "Bearer #{Setting.three_sixty_dialog_partner_token}" + } + request = Net::HTTP::Get.new(url.to_s, headers) + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| + http.request(request) + end + channels_array = JSON.parse(response.body)['partner_channels'] + client_hash = channels_array.find { |hash| hash['client']['id'] == Setting.three_sixty_dialog_client_id } + waba_account_id = client_hash['waba_account']['external_id'] + Setting.three_sixty_dialog_client_waba_account_id = waba_account_id + end + + # rubocop:disable Metrics/MethodLength + def template_payload + { + name: template_name, + category: 'MARKETING', + components: [ + { + type: 'BODY', + text: template_text, + example: { + body_text: [ + [ + 'Jakob', + 'Familie und Freizeit' + ] + ] + } + }, + { + type: 'BUTTONS', + buttons: [ + { + type: 'QUICK_REPLY', + text: 'Antworten' + }, + { + type: 'QUICK_REPLY', + text: 'Mehr Infos' + } + ] + } + ], + language: 'de', + allow_category_change: true + } + end + # rubocop:enable Metrics/MethodLength + + def handle_response(response) + case response.code.to_i + when 200 + Rails.logger.debug 'Great!' + when 400..599 + exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) + ErrorNotifier.report(exception) + end + end + end +end diff --git a/app/jobs/whats_app_adapter/set_webhook_url.rb b/app/jobs/whats_app_adapter/set_webhook_url.rb new file mode 100644 index 000000000..f2be9e8ce --- /dev/null +++ b/app/jobs/whats_app_adapter/set_webhook_url.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module WhatsAppAdapter + class SetWebhookUrl < ApplicationJob + def perform + return unless Setting.three_sixty_dialog_api_key + + base_uri = Setting.three_sixty_dialog_whats_app_rest_api_endpoint + url = URI.parse("#{base_uri}/configs/webhook") + headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_api_key, 'Content-Type' => 'application/json' } + request = Net::HTTP::Post.new(url.to_s, headers) + + request.body = { url: "https://#{Setting.application_host}/whats_app/three-sixty-dialog-webhook" }.to_json + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| + http.request(request) + end + handle_response(response) + end + + private + + def handle_response(response) + case response.code.to_i + when 200 + Rails.logger.debug 'Great!' + when 400..599 + exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) + ErrorNotifier.report(exception) + end + end + end +end diff --git a/lib/tasks/whats_app/create_templates.rake b/lib/tasks/whats_app/create_templates.rake new file mode 100644 index 000000000..fe0d6b7b2 --- /dev/null +++ b/lib/tasks/whats_app/create_templates.rake @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +desc 'Create WhatsApp templates' +task create_whats_app_templates: :environment do + template_hash = I18n.t('.')[:adapter][:whats_app][:request_template] + template_hash.each do |key, value| + WhatsAppAdapter::CreateTemplate.perform_later(template_name: key, + template_text: value.gsub('%s', '{{1}}').gsub( + '%s', '{{2}}' + )) + end +end From 74be2606c9599329b7b057f50524013c7c6ff9e2 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 1 Aug 2023 17:32:14 +0200 Subject: [PATCH 07/57] Start receive message feature 360dialog --- .../three_sixty_dialog_inbound.rb | 147 ++++++++++++++++++ .../whats_app/webhook_controller.rb | 44 ++++++ config/routes.rb | 1 + 3 files changed, 192 insertions(+) create mode 100644 app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb new file mode 100644 index 000000000..b1aa64e5f --- /dev/null +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +module WhatsAppAdapter + UNKNOWN_CONTRIBUTOR = :unknown_contributor + UNSUPPORTED_CONTENT = :unsupported_content + REQUEST_FOR_MORE_INFO = :request_for_more_info + REQUEST_TO_RECEIVE_MESSAGE = :request_to_receive_message + UNSUBSCRIBE_CONTRIBUTOR = :unsubscribe_contributor + SUBSCRIBE_CONTRIBUTOR = :subscribe_contributor + + class ThreeSixtyDialogInbound + SUPPORTED_ATTACHMENT_TYPES = %w[image/jpg image/jpeg image/png image/gif audio/ogg video/mp4].freeze + UNSUPPORTED_CONTENT_TYPES = %w[application text/vcard latitude longitude].freeze + + attr_reader :sender, :text, :message + + def initialize + @callbacks = {} + end + + def on(callback, &block) + @callbacks[callback] = block + end + + def consume(whats_app_message) + whats_app_message = whats_app_message.with_indifferent_access + + @sender = initialize_sender(whats_app_message) + return unless @sender + + @message = initialize_message(whats_app_message) + return unless @message + + @unsupported_content = initialize_unsupported_content(whats_app_message) + + files = initialize_file(whats_app_message) + @message.files = files + + return unless create_message? + + yield(@message) if block_given? + end + + private + + def trigger(event, *args) + return unless @callbacks.key?(event) + + @callbacks[event].call(*args) + end + + def initialize_sender(whats_app_message) + whats_app_phone_number = whats_app_message[:contacts].first[:wa_id].phony_normalized + sender = Contributor.find_by(whats_app_phone_number: whats_app_phone_number) + + unless sender + trigger(UNKNOWN_CONTRIBUTOR, whats_app_phone_number) + return nil + end + + sender + end + + def initialize_message(whats_app_message) + message_text = whats_app_message[:messages].first[:text][:body] + + trigger(REQUEST_FOR_MORE_INFO, sender) if request_for_more_info?(message_text) + trigger(UNSUBSCRIBE_CONTRIBUTOR, sender) if unsubscribe_text?(message_text) + trigger(SUBSCRIBE_CONTRIBUTOR, sender) if subscribe_text?(message_text) + trigger(REQUEST_TO_RECEIVE_MESSAGE, sender) if request_to_receive_message?(sender, whats_app_message) + + message = Message.new(text: message_text, sender: sender) + message.raw_data.attach( + io: StringIO.new(JSON.generate(whats_app_message)), + filename: 'whats_app_message.json', + content_type: 'application/json' + ) + message + end + + def initialize_unsupported_content(whats_app_message) + return unless unsupported_content?(whats_app_message) + + message.unknown_content = true + trigger(UNSUPPORTED_CONTENT, sender) + end + + def initialize_file(whats_app_message) + return [] unless whats_app_message[:media_content_type0] && whats_app_message[:media_url0] + + file = Message::File.new + + content_type = whats_app_message[:media_content_type0] + media_url = whats_app_message[:media_url0] + filename = media_url.split('/Media/').last + + file.attachment.attach( + io: URI.parse(media_url).open, + filename: filename, + content_type: content_type, + identify: false + ) + + [file] + end + + def unsupported_content?(whats_app_message) + whats_app_message.keys.any? { |key| UNSUPPORTED_CONTENT_TYPES.include?(key) } || whats_app_message.any? do |key, value| + key.match?(/media_content_type/) && UNSUPPORTED_CONTENT_TYPES.any? { |content_type| value.match?(/#{content_type}/) } + end + end + + def request_for_more_info?(text) + text.strip.eql?(I18n.t('adapter.whats_app.quick_reply_button_text.more_info')) + end + + def request_to_receive_message?(contributor, whats_app_message) + text = whats_app_message[:messages].first[:text][:body] + return false if request_for_more_info?(text) || unsubscribe_text?(text) || subscribe_text?(text) + + contributor.whats_app_message_template_sent_at.present? + end + + def quick_reply_response?(text) + quick_reply_keys = %w[answer more_info] + quick_reply_texts = [] + quick_reply_keys.each do |key| + quick_reply_texts << I18n.t("adapter.whats_app.quick_reply_button_text.#{key}") + end + text.strip.in?(quick_reply_texts) + end + + def unsubscribe_text?(text) + text.downcase.strip.eql?(I18n.t('adapter.whats_app.unsubscribe.text')) + end + + def subscribe_text?(text) + text.downcase.strip.eql?(I18n.t('adapter.whats_app.subscribe.text')) + end + + def create_message? + has_non_text_content = message.files.any? || message.unknown_content + text = message.text + has_non_text_content || (message.text && !quick_reply_response?(text) && !unsubscribe_text?(text) && !subscribe_text?(text)) + end + end +end diff --git a/app/controllers/whats_app/webhook_controller.rb b/app/controllers/whats_app/webhook_controller.rb index 942389058..916d0c1dd 100644 --- a/app/controllers/whats_app/webhook_controller.rb +++ b/app/controllers/whats_app/webhook_controller.rb @@ -36,6 +36,37 @@ def message adapter.consume(whats_app_message_params) { |message| message.contributor.reply(adapter) } end + def three_sixty_dialog_message + head :ok + adapter = WhatsAppAdapter::ThreeSixtyDialogInbound.new + + adapter.on(WhatsAppAdapter::UNKNOWN_CONTRIBUTOR) do |whats_app_phone_number| + handle_unknown_contributor(whats_app_phone_number) + end + + adapter.on(WhatsAppAdapter::REQUEST_FOR_MORE_INFO) do |contributor| + handle_request_for_more_info(contributor) + end + + adapter.on(WhatsAppAdapter::REQUEST_TO_RECEIVE_MESSAGE) do |contributor| + handle_request_to_receive_message_three_sixty_dialog(contributor) + end + + adapter.on(WhatsAppAdapter::UNSUPPORTED_CONTENT) do |contributor| + WhatsAppAdapter::Outbound.send_unsupported_content_message!(contributor) + end + + adapter.on(WhatsAppAdapter::UNSUBSCRIBE_CONTRIBUTOR) do |contributor| + handle_unsubsribe_contributor(contributor) + end + + adapter.on(WhatsAppAdapter::SUBSCRIBE_CONTRIBUTOR) do |contributor| + handle_subscribe_contributor(contributor) + end + + adapter.consume(three_sixty_dialog_message_params.to_h) { |message| message.contributor.reply(adapter) } + end + def errors return unless error_params['Level'] == 'ERROR' @@ -73,6 +104,13 @@ def message_params :ReferralNumMedia, :SmsMessageSid, :SmsSid, :SmsStatus, :To, :WaId) end + def three_sixty_dialog_message_params + params.permit({ webhook: [contacts: [:wa_id, { profile: [:name] }], + messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }]] }, + contacts: [:wa_id, { profile: [:name] }], + messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }]) + end + def error_params params.permit(:AccountSid, :Level, :ParentAccountSid, :Payload, :PayloadType, :Sid, :Timestamp) end @@ -100,6 +138,12 @@ def handle_request_to_receive_message(contributor, twilio_message_sid) WhatsAppAdapter::Outbound.send!(message || contributor.received_messages.first) end + def handle_request_to_receive_message_three_sixty_dialog(contributor) + contributor.update!(whats_app_message_template_responded_at: Time.current, whats_app_message_template_sent_at: nil) + + WhatsAppAdapter::Outbound.send!(contributor.received_messages.first) + end + def handle_unsubsribe_contributor(contributor) contributor.update!(deactivated_at: Time.current) diff --git a/config/routes.rb b/config/routes.rb index f98f2b418..8e95757d7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -50,6 +50,7 @@ post '/status', to: 'webhook#status' post '/three-sixty-dialog-status', to: 'webhook#three_sixty_dialog_status' get '/onboarding-successful', to: 'onboarding#success' + post '/three-sixty-dialog-webhook', to: 'webhook#three_sixty_dialog_message' end telegram_webhook Telegram::WebhookController From d68c395d2b7cc9a46f6c6f45112d46581dac59e2 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 1 Aug 2023 19:06:45 +0200 Subject: [PATCH 08/57] Send WhatsApp message with configured BPA - At the moment, we have some legacy clients with Twilio, but are planning to onboard new clients with 360dialog. We check if we have an 360dialog API key for the client to determine which implementation to use. --- app/adapters/whats_app_adapter/outbound.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index 8f9fbf668..84302577e 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -77,7 +77,13 @@ def freeform_message_permitted?(recipient) def send_message_template(recipient, message) recipient.update(whats_app_message_template_sent_at: Time.current) - WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: template_payload(recipient, message)) + if Setting.three_sixty_dialog_api_key.present? + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: template_payload(recipient, message)) + else + text = I18n.t("adapter.whats_app.request_template.new_request_#{time_of_day}_#{rand(1..3)}", first_name: recipient.first_name, + request_title: message.request.title) + WhatsAppAdapter::Outbound::Text.perform_later(recipient: recipient, text: text) + end end def send_message(recipient, message) From 7f9a8437382dda38dc27abb94b2466f7af28f9fc Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 1 Aug 2023 20:18:17 +0200 Subject: [PATCH 09/57] Add 360dialog to other outbound methods --- app/adapters/whats_app_adapter/outbound.rb | 96 +++++++++++++++---- .../outbound/three_sixty_dialog_text.rb | 15 ++- 2 files changed, 90 insertions(+), 21 deletions(-) diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index 84302577e..b4f874e6e 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ClassLength module WhatsAppAdapter class Outbound class << self @@ -18,30 +19,50 @@ def send_welcome_message!(contributor) return unless contributor_can_receive_messages?(contributor) welcome_message = I18n.t('adapter.whats_app.welcome_message', project_name: Setting.project_name) - WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: welcome_message) + if Setting.three_sixty_dialog_api_key.present? + payload = if freeform_message_permitted?(contributor) + text_payload(contributor, welcome_message) + else + welcome_message_payload(contributor) + end + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: payload) + else + WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: welcome_message) + end end def send_unsupported_content_message!(contributor) return unless contributor_can_receive_messages?(contributor) - WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, - text: I18n.t('adapter.whats_app.unsupported_content_template', - first_name: contributor.first_name, - contact_person: contributor.organization.contact_person.name)) + text = I18n.t('adapter.whats_app.unsupported_content_template', first_name: contributor.first_name, + contact_person: contributor.organization.contact_person.name) + if Setting.three_sixty_dialog_api_key.present? + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) + else + WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) + end end def send_more_info_message!(contributor) return unless contributor_can_receive_messages?(contributor) text = [Setting.about, "_#{I18n.t('adapter.whats_app.unsubscribe.instructions')}_"].join("\n\n") - WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) + if Setting.three_sixty_dialog_api_key.present? + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) + else + WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) + end end def send_unsubsribed_successfully_message!(contributor) return unless contributor_can_receive_messages?(contributor) text = [I18n.t('adapter.whats_app.unsubscribe.successful'), "_#{I18n.t('adapter.whats_app.subscribe.instructions')}_"].join("\n\n") - WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) + if Setting.three_sixty_dialog_api_key.present? + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) + else + WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) + end end def contributor_can_receive_messages?(recipient) @@ -78,7 +99,7 @@ def freeform_message_permitted?(recipient) def send_message_template(recipient, message) recipient.update(whats_app_message_template_sent_at: Time.current) if Setting.three_sixty_dialog_api_key.present? - WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: template_payload(recipient, message)) + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: new_request_payload(recipient, message.request.title)) else text = I18n.t("adapter.whats_app.request_template.new_request_#{time_of_day}_#{rand(1..3)}", first_name: recipient.first_name, request_title: message.request.title) @@ -90,7 +111,11 @@ def send_message(recipient, message) files = message.files if files.blank? - WhatsAppAdapter::Outbound::Text.perform_later(recipient: recipient, text: message.text) + if Setting.three_sixty_dialog_api_key.present? + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(recipient, message.text)) + else + WhatsAppAdapter::Outbound::Text.perform_later + end else files.each_with_index do |file, index| WhatsAppAdapter::Outbound::File.perform_later(recipient: recipient, text: index.zero? ? message.text : '', file: file) @@ -98,14 +123,14 @@ def send_message(recipient, message) end end - def template_payload(recipient, message) - template_namespace = Setting.three_sixty_dialog_whats_app_template_namespace + # rubocop:disable Metrics/MethodLength + def new_request_payload(recipient, request_title) { recipient_type: 'individual', to: recipient.whats_app_phone_number.split('+').last, type: 'template', template: { - namespace: template_namespace, + namespace: Setting.three_sixty_dialog_whats_app_template_namespace, language: { policy: 'deterministic', code: 'de' @@ -113,18 +138,49 @@ def template_payload(recipient, message) name: 'new_request_morning_1', # TODO: Use dynamic template name after WhatsAppAdapter::CreateTemplate works components: [{ type: 'body', - parameters: [{ - type: 'text', - text: recipient.first_name - }, - { - type: 'text', - text: message.request.title - }] + parameters: [ + { + type: 'text', + text: recipient.first_name + }, + { + type: 'text', + text: request_title + } + ] }] } } end + # rubocop:enable Metrics/MethodLength + + def text_payload(recipient, text) + { + recipient_type: 'individual', + to: recipient.whats_app_phone_number.split('+').last, + type: 'text', + text: { + body: text + } + } + end + + def welcome_message_payload(recipient) + { + recipient_type: 'individual', + to: recipient.whats_app_phone_number.split('+').last, + type: 'template', + template: { + namespace: template_namespace, + language: { + policy: 'deterministic', + code: 'de' + }, + name: 'welcome_message' + } + } + end end end end +# rubocop:enable Metrics/ClassLength diff --git a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb index 7b7d6ad59..4d9458a8e 100644 --- a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb +++ b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb @@ -14,7 +14,20 @@ def perform(payload:) response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| http.request(request) end - response.value # may raise exception + handle_response(response) + end + + private + + def handle_response(response) + case response.code.to_i + when 200 + Rails.logger.debug 'Great!' + when 400..599 + exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) + Rails.logger.debug response.body.to_s + ErrorNotifier.report(exception) + end end end end From 22a420a8149aae0fd6db50423bac19305cb84f9b Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 1 Aug 2023 20:18:45 +0200 Subject: [PATCH 10/57] Handle other inbound text actions - request for more info - request to unsubscribe - request to re-subscribe --- .../three_sixty_dialog_inbound.rb | 24 ++++++++++++------- .../whats_app/webhook_controller.rb | 7 ++++-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb index b1aa64e5f..076701e40 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb @@ -62,14 +62,15 @@ def initialize_sender(whats_app_message) end def initialize_message(whats_app_message) - message_text = whats_app_message[:messages].first[:text][:body] + message = whats_app_message[:messages].first + text = message[:text]&.dig(:body) || message[:button]&.dig(:text) - trigger(REQUEST_FOR_MORE_INFO, sender) if request_for_more_info?(message_text) - trigger(UNSUBSCRIBE_CONTRIBUTOR, sender) if unsubscribe_text?(message_text) - trigger(SUBSCRIBE_CONTRIBUTOR, sender) if subscribe_text?(message_text) - trigger(REQUEST_TO_RECEIVE_MESSAGE, sender) if request_to_receive_message?(sender, whats_app_message) + trigger(REQUEST_FOR_MORE_INFO, sender) if request_for_more_info?(text) + trigger(UNSUBSCRIBE_CONTRIBUTOR, sender) if unsubscribe_text?(text) + trigger(SUBSCRIBE_CONTRIBUTOR, sender) if subscribe_text?(text) + trigger(REQUEST_TO_RECEIVE_MESSAGE, sender) if request_to_receive_message?(sender, text) - message = Message.new(text: message_text, sender: sender) + message = Message.new(text: text, sender: sender) message.raw_data.attach( io: StringIO.new(JSON.generate(whats_app_message)), filename: 'whats_app_message.json', @@ -111,11 +112,12 @@ def unsupported_content?(whats_app_message) end def request_for_more_info?(text) + return false if text.blank? + text.strip.eql?(I18n.t('adapter.whats_app.quick_reply_button_text.more_info')) end - def request_to_receive_message?(contributor, whats_app_message) - text = whats_app_message[:messages].first[:text][:body] + def request_to_receive_message?(contributor, text) return false if request_for_more_info?(text) || unsubscribe_text?(text) || subscribe_text?(text) contributor.whats_app_message_template_sent_at.present? @@ -131,17 +133,21 @@ def quick_reply_response?(text) end def unsubscribe_text?(text) + return false if text.blank? + text.downcase.strip.eql?(I18n.t('adapter.whats_app.unsubscribe.text')) end def subscribe_text?(text) + return false if text.blank? + text.downcase.strip.eql?(I18n.t('adapter.whats_app.subscribe.text')) end def create_message? has_non_text_content = message.files.any? || message.unknown_content text = message.text - has_non_text_content || (message.text && !quick_reply_response?(text) && !unsubscribe_text?(text) && !subscribe_text?(text)) + has_non_text_content || (message.text.present? && !quick_reply_response?(text) && !unsubscribe_text?(text) && !subscribe_text?(text)) end end end diff --git a/app/controllers/whats_app/webhook_controller.rb b/app/controllers/whats_app/webhook_controller.rb index 916d0c1dd..725cb57f9 100644 --- a/app/controllers/whats_app/webhook_controller.rb +++ b/app/controllers/whats_app/webhook_controller.rb @@ -38,6 +38,8 @@ def message def three_sixty_dialog_message head :ok + return if params['statuses'].present? # TODO: Do we want to handle statuses? + adapter = WhatsAppAdapter::ThreeSixtyDialogInbound.new adapter.on(WhatsAppAdapter::UNKNOWN_CONTRIBUTOR) do |whats_app_phone_number| @@ -106,9 +108,10 @@ def message_params def three_sixty_dialog_message_params params.permit({ webhook: [contacts: [:wa_id, { profile: [:name] }], - messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }]] }, + messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, + { button: [:text] }]] }, contacts: [:wa_id, { profile: [:name] }], - messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }]) + messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, { button: [:text] }]) end def error_params From c8e71c155c72fc4f65796e3d349d17fb1b9be40f Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 2 Aug 2023 16:57:07 +0200 Subject: [PATCH 11/57] Fix template creation workflow - it should be id, not external_id - handle error trying to create duplicate template name --- app/jobs/whats_app_adapter/create_template.rb | 7 ++++--- app/jobs/whats_app_adapter/set_webhook_url.rb | 2 ++ lib/tasks/whats_app/create_templates.rake | 6 ++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/jobs/whats_app_adapter/create_template.rb b/app/jobs/whats_app_adapter/create_template.rb index 9d5bb23b7..d7e391f8e 100644 --- a/app/jobs/whats_app_adapter/create_template.rb +++ b/app/jobs/whats_app_adapter/create_template.rb @@ -35,7 +35,6 @@ def perform(template_name:, template_text:) def set_headers { - 'D360-API-KEY': Setting.three_sixty_dialog_api_key, 'Content-Type': 'application/json', Authorization: "Bearer #{Setting.three_sixty_dialog_partner_token}" } @@ -69,7 +68,7 @@ def fetch_waba_account_id end channels_array = JSON.parse(response.body)['partner_channels'] client_hash = channels_array.find { |hash| hash['client']['id'] == Setting.three_sixty_dialog_client_id } - waba_account_id = client_hash['waba_account']['external_id'] + waba_account_id = client_hash['waba_account']['id'] Setting.three_sixty_dialog_client_waba_account_id = waba_account_id end @@ -113,9 +112,11 @@ def template_payload def handle_response(response) case response.code.to_i - when 200 + when 201 Rails.logger.debug 'Great!' when 400..599 + return if response.body.match?(/you have provided is already in use. Please choose a different name for your template./) + exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) ErrorNotifier.report(exception) end diff --git a/app/jobs/whats_app_adapter/set_webhook_url.rb b/app/jobs/whats_app_adapter/set_webhook_url.rb index f2be9e8ce..b86167201 100644 --- a/app/jobs/whats_app_adapter/set_webhook_url.rb +++ b/app/jobs/whats_app_adapter/set_webhook_url.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'net/http' + module WhatsAppAdapter class SetWebhookUrl < ApplicationJob def perform diff --git a/lib/tasks/whats_app/create_templates.rake b/lib/tasks/whats_app/create_templates.rake index fe0d6b7b2..6c1c83ffa 100644 --- a/lib/tasks/whats_app/create_templates.rake +++ b/lib/tasks/whats_app/create_templates.rake @@ -1,12 +1,14 @@ # frozen_string_literal: true +# rubocop:disable Style/FormatStringToken desc 'Create WhatsApp templates' task create_whats_app_templates: :environment do template_hash = I18n.t('.')[:adapter][:whats_app][:request_template] template_hash.each do |key, value| WhatsAppAdapter::CreateTemplate.perform_later(template_name: key, - template_text: value.gsub('%s', '{{1}}').gsub( - '%s', '{{2}}' + template_text: value.gsub('%{first_name}', '{{1}}').gsub( + '%{request_title}', '{{2}}' )) end end +# rubocop:enable Style/FormatStringToken From a1701fb893b07e67d486b662a1e2e9ade6fdd5a6 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 2 Aug 2023 17:21:21 +0200 Subject: [PATCH 12/57] Add sending image files feature to 360dialog WhatsApp --- app/adapters/whats_app_adapter/outbound.rb | 36 ++++++++------ .../outbound/three_sixty_dialog_file.rb | 49 +++++++++++++++++++ .../outbound/three_sixty_dialog_text.rb | 1 - app/jobs/whats_app_adapter/upload_file.rb | 45 +++++++++++++++++ 4 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 app/adapters/whats_app_adapter/outbound/three_sixty_dialog_file.rb create mode 100644 app/jobs/whats_app_adapter/upload_file.rb diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index b4f874e6e..0f3f0112a 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -99,7 +99,7 @@ def freeform_message_permitted?(recipient) def send_message_template(recipient, message) recipient.update(whats_app_message_template_sent_at: Time.current) if Setting.three_sixty_dialog_api_key.present? - WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: new_request_payload(recipient, message.request.title)) + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: new_request_payload(recipient, message.request)) else text = I18n.t("adapter.whats_app.request_template.new_request_#{time_of_day}_#{rand(1..3)}", first_name: recipient.first_name, request_title: message.request.title) @@ -116,6 +116,10 @@ def send_message(recipient, message) else WhatsAppAdapter::Outbound::Text.perform_later end + elsif Setting.three_sixty_dialog_api_key.present? + files.each do |_file| + WhatsAppAdapter::UploadFile.perform_later(message_id: message.id) + end else files.each_with_index do |file, index| WhatsAppAdapter::Outbound::File.perform_later(recipient: recipient, text: index.zero? ? message.text : '', file: file) @@ -124,7 +128,7 @@ def send_message(recipient, message) end # rubocop:disable Metrics/MethodLength - def new_request_payload(recipient, request_title) + def new_request_payload(recipient, request) { recipient_type: 'individual', to: recipient.whats_app_phone_number.split('+').last, @@ -136,19 +140,21 @@ def new_request_payload(recipient, request_title) code: 'de' }, name: 'new_request_morning_1', # TODO: Use dynamic template name after WhatsAppAdapter::CreateTemplate works - components: [{ - type: 'body', - parameters: [ - { - type: 'text', - text: recipient.first_name - }, - { - type: 'text', - text: request_title - } - ] - }] + components: [ + { + type: 'body', + parameters: [ + { + type: 'text', + text: recipient.first_name + }, + { + type: 'text', + text: request.title + } + ] + } + ] } } end diff --git a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_file.rb b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_file.rb new file mode 100644 index 000000000..c69287840 --- /dev/null +++ b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_file.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module WhatsAppAdapter + class Outbound + class ThreeSixtyDialogFile < ApplicationJob + queue_as :default + + def perform(message_id:, file_id:) + message = Message.find(message_id) + @recipient = message.recipient + @file_id = file_id + + url = URI.parse("#{Setting.three_sixty_dialog_whats_app_rest_api_endpoint}/messages") + headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_api_key, 'Content-Type' => 'application/json' } + request = Net::HTTP::Post.new(url.to_s, headers) + request.body = payload.to_json + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| + http.request(request) + end + handle_response(response) + end + + private + + attr_reader :recipient, :file_id + + def payload + { + recipient_type: 'individual', + to: recipient.whats_app_phone_number.split('+').last, + type: 'image', + image: { + id: file_id + } + } + end + + def handle_response(response) + case response.code.to_i + when 200 + Rails.logger.debug 'Great!' + when 400..599 + exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) + ErrorNotifier.report(exception) + end + end + end + end +end diff --git a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb index 4d9458a8e..8515da5f7 100644 --- a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb +++ b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb @@ -25,7 +25,6 @@ def handle_response(response) Rails.logger.debug 'Great!' when 400..599 exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) - Rails.logger.debug response.body.to_s ErrorNotifier.report(exception) end end diff --git a/app/jobs/whats_app_adapter/upload_file.rb b/app/jobs/whats_app_adapter/upload_file.rb new file mode 100644 index 000000000..3c704485d --- /dev/null +++ b/app/jobs/whats_app_adapter/upload_file.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'net/http' + +module WhatsAppAdapter + class UploadFile < ApplicationJob + def perform(message_id:) + return if Setting.three_sixty_dialog_api_key.blank? + + @message_id = message_id + message = Message.find(message_id) + request = message.request + + request.files.each do |file| + base_uri = Setting.three_sixty_dialog_whats_app_rest_api_endpoint + url = URI.parse("#{base_uri}/media") + headers = { + 'D360-API-KEY': Setting.three_sixty_dialog_api_key, + 'Content-Type': file.blob.content_type + } + request = Net::HTTP::Post.new(url.to_s, headers) + request.body = File.read(ActiveStorage::Blob.service.path_for(file.blob.key)) + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| + http.request(request) + end + handle_response(response) + end + end + + private + + attr_reader :message_id + + def handle_response(response) + case response.code.to_i + when 201 + file_id = JSON.parse(response.body)['media'].first['id'] + WhatsAppAdapter::Outbound::ThreeSixtyDialogFile.perform_later(message_id: message_id, file_id: file_id) + when 400..599 + exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) + ErrorNotifier.report(exception) + end + end + end +end From 43c7db0ef98e17aea7470070a4d4cba24f596a7a Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 3 Aug 2023 12:08:48 +0200 Subject: [PATCH 13/57] Conditionally create template, add welcome message - Fetch the existing templates and do not attempt to create a template with the same name to avoid predictable errors --- app/jobs/whats_app_adapter/create_template.rb | 60 +++++++++++++++---- lib/tasks/whats_app/create_templates.rake | 11 ++-- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/app/jobs/whats_app_adapter/create_template.rb b/app/jobs/whats_app_adapter/create_template.rb index d7e391f8e..ff7825ab6 100644 --- a/app/jobs/whats_app_adapter/create_template.rb +++ b/app/jobs/whats_app_adapter/create_template.rb @@ -10,29 +10,49 @@ def perform(template_name:, template_text:) @template_name = template_name @template_text = template_text - token = Setting.find_by(var: 'three_sixty_dialog_partner_token') - fetch_token unless token&.value && token.updated_at > 24.hours.ago + @token = Setting.find_by(var: 'three_sixty_dialog_partner_token') + @token = fetch_token unless token&.value && token.updated_at > 24.hours.ago - waba_account_id = Setting.three_sixty_dialog_client_waba_account_id - waba_account_id = fetch_waba_account_id if waba_account_id.blank? + @waba_account_id = Setting.three_sixty_dialog_client_waba_account_id + @waba_account_id = fetch_waba_account_id if waba_account_id.blank? + conditionally_create_template + end + + attr_reader :base_uri, :partner_id, :template_name, :template_text, :token, :waba_account_id + + private + + def conditionally_create_template + url = URI.parse( + "#{base_uri}/partners/#{partner_id}/waba_accounts/#{waba_account_id}/waba_templates" + ) + headers = set_headers + request = Net::HTTP::Get.new(url.to_s, headers) + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| + http.request(request) + end + waba_templates = JSON.parse(response.body)['waba_templates'] + template_names_array = waba_templates.pluck('name') + return if template_name.in?(template_names_array) + create_template + end + + def create_template url = URI.parse( "#{base_uri}/partners/#{partner_id}/waba_accounts/#{waba_account_id}/waba_templates" ) headers = set_headers request = Net::HTTP::Post.new(url.to_s, headers) - request.body = template_payload.to_json + payload = template_name.match?(/welcome_message/) ? welcome_message_template_payload : new_request_template_payload + request.body = payload.to_json response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| http.request(request) end handle_response(response) end - attr_reader :base_uri, :partner_id, :template_name, :template_text - - private - def set_headers { 'Content-Type': 'application/json', @@ -73,7 +93,7 @@ def fetch_waba_account_id end # rubocop:disable Metrics/MethodLength - def template_payload + def new_request_template_payload { name: template_name, category: 'MARKETING', @@ -110,6 +130,26 @@ def template_payload end # rubocop:enable Metrics/MethodLength + def welcome_message_template_payload + { + name: template_name, + category: 'MARKETING', + components: [ + { + type: 'BODY', + text: template_text, + example: { + body_text: [ + ['100eyes'] + ] + } + } + ], + language: 'de', + allow_category_change: true + } + end + def handle_response(response) case response.code.to_i when 201 diff --git a/lib/tasks/whats_app/create_templates.rake b/lib/tasks/whats_app/create_templates.rake index 6c1c83ffa..ab3381f33 100644 --- a/lib/tasks/whats_app/create_templates.rake +++ b/lib/tasks/whats_app/create_templates.rake @@ -3,12 +3,13 @@ # rubocop:disable Style/FormatStringToken desc 'Create WhatsApp templates' task create_whats_app_templates: :environment do - template_hash = I18n.t('.')[:adapter][:whats_app][:request_template] + welcome_message_hash = { welcome_message: I18n.t('.')[:adapter][:whats_app][:welcome_message].gsub('%{project_name}', '{{1}}') } + requests_hash = I18n.t('.')[:adapter][:whats_app][:request_template].transform_values do |value| + value.gsub('%{first_name}', '{{1}}').gsub('%{request_title}', '{{2}}') + end + template_hash = welcome_message_hash.merge(requests_hash) template_hash.each do |key, value| - WhatsAppAdapter::CreateTemplate.perform_later(template_name: key, - template_text: value.gsub('%{first_name}', '{{1}}').gsub( - '%{request_title}', '{{2}}' - )) + WhatsAppAdapter::CreateTemplate.perform_later(template_name: key, template_text: value) end end # rubocop:enable Style/FormatStringToken From e0932f345fb138724a1de4d79175637d8cd7e327 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 3 Aug 2023 13:20:48 +0200 Subject: [PATCH 14/57] Add support for receiving files - image/audio/video --- .../three_sixty_dialog_inbound.rb | 23 ++++++++++++++----- .../whats_app/webhook_controller.rb | 12 ++++------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb index 076701e40..042aa3050 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb @@ -87,17 +87,18 @@ def initialize_unsupported_content(whats_app_message) end def initialize_file(whats_app_message) - return [] unless whats_app_message[:media_content_type0] && whats_app_message[:media_url0] + message = whats_app_message[:messages].first + return [] unless message[:image] || message[:voice] || message[:video] file = Message::File.new - content_type = whats_app_message[:media_content_type0] - media_url = whats_app_message[:media_url0] - filename = media_url.split('/Media/').last + message_file = message[:image] || message[:voice] || message[:video] + content_type = message_file[:mime_type] + file_id = message_file[:id] file.attachment.attach( - io: URI.parse(media_url).open, - filename: filename, + io: StringIO.new(fetch_file(file_id)), + filename: file_id, content_type: content_type, identify: false ) @@ -149,5 +150,15 @@ def create_message? text = message.text has_non_text_content || (message.text.present? && !quick_reply_response?(text) && !unsubscribe_text?(text) && !subscribe_text?(text)) end + + def fetch_file(file_id) + url = URI.parse("#{Setting.three_sixty_dialog_whats_app_rest_api_endpoint}/media/#{file_id}") + headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_api_key, 'Content-Type' => 'application/json' } + request = Net::HTTP::Get.new(url.to_s, headers) + response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| + http.request(request) + end + response.body + end end end diff --git a/app/controllers/whats_app/webhook_controller.rb b/app/controllers/whats_app/webhook_controller.rb index 725cb57f9..c709b32af 100644 --- a/app/controllers/whats_app/webhook_controller.rb +++ b/app/controllers/whats_app/webhook_controller.rb @@ -92,11 +92,6 @@ def status ErrorNotifier.report(exception, context: { message_sid: status_params['MessageSid'] }) end - def three_sixty_dialog_status - Rails.logger.debug params - head :ok - end - private def message_params @@ -109,9 +104,12 @@ def message_params def three_sixty_dialog_message_params params.permit({ webhook: [contacts: [:wa_id, { profile: [:name] }], messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, - { button: [:text] }]] }, + { button: [:text] }, { image: %i[id mime_type sha256] }, { voice: %i[id mime_type sha256] }, + { video: %i[id mime_type sha256] }]] }, contacts: [:wa_id, { profile: [:name] }], - messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, { button: [:text] }]) + messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, { button: [:text] }, + { image: %i[id mime_type sha256] }, { voice: %i[id mime_type sha256] }, + { video: %i[id mime_type sha256] }]) end def error_params From 14ab59d8a7b488cd4cad0aa096541d999006d5df Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 3 Aug 2023 14:24:10 +0200 Subject: [PATCH 15/57] Handle unsupported content --- .../three_sixty_dialog_inbound.rb | 13 ++++++---- .../whats_app/webhook_controller.rb | 24 +++++++++++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb index 042aa3050..2ad80148b 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb @@ -10,7 +10,7 @@ module WhatsAppAdapter class ThreeSixtyDialogInbound SUPPORTED_ATTACHMENT_TYPES = %w[image/jpg image/jpeg image/png image/gif audio/ogg video/mp4].freeze - UNSUPPORTED_CONTENT_TYPES = %w[application text/vcard latitude longitude].freeze + UNSUPPORTED_CONTENT_TYPES = %w[location contacts].freeze attr_reader :sender, :text, :message @@ -107,9 +107,14 @@ def initialize_file(whats_app_message) end def unsupported_content?(whats_app_message) - whats_app_message.keys.any? { |key| UNSUPPORTED_CONTENT_TYPES.include?(key) } || whats_app_message.any? do |key, value| - key.match?(/media_content_type/) && UNSUPPORTED_CONTENT_TYPES.any? { |content_type| value.match?(/#{content_type}/) } - end + message = whats_app_message[:messages].first + return unless message + + errors = message[:errors] + ((errors && errors.first[:title].match?(/Unsupported message type/)) || errors.first[:title].match?(/Received Wrong Message Type/)) || + message.keys.any? do |key| + UNSUPPORTED_CONTENT_TYPES.include?(key) + end end def request_for_more_info?(text) diff --git a/app/controllers/whats_app/webhook_controller.rb b/app/controllers/whats_app/webhook_controller.rb index c709b32af..2845c4c77 100644 --- a/app/controllers/whats_app/webhook_controller.rb +++ b/app/controllers/whats_app/webhook_controller.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ClassLength, Metrics/AbcSize +# TODO: Separate WhatsApp implementations intro separate controllers module WhatsApp class WebhookController < ApplicationController skip_before_action :require_login, :verify_authenticity_token @@ -40,6 +42,8 @@ def three_sixty_dialog_message head :ok return if params['statuses'].present? # TODO: Do we want to handle statuses? + handle_error(params['errors']) if params['errors'].present? + adapter = WhatsAppAdapter::ThreeSixtyDialogInbound.new adapter.on(WhatsAppAdapter::UNKNOWN_CONTRIBUTOR) do |whats_app_phone_number| @@ -105,11 +109,21 @@ def three_sixty_dialog_message_params params.permit({ webhook: [contacts: [:wa_id, { profile: [:name] }], messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, { button: [:text] }, { image: %i[id mime_type sha256] }, { voice: %i[id mime_type sha256] }, - { video: %i[id mime_type sha256] }]] }, + { video: %i[id mime_type sha256] }, + { errors: %i[code details title] }, + { location: %i[latitude longitude timestamp type] }, + { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, + { phones: %i[phone type wa_id] }, { urls: [] }, + { name: %i[first_name formatted_name last_name] }] }]] }, contacts: [:wa_id, { profile: [:name] }], messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, { button: [:text] }, { image: %i[id mime_type sha256] }, { voice: %i[id mime_type sha256] }, - { video: %i[id mime_type sha256] }]) + { video: %i[id mime_type sha256] }, + { errors: %i[code details title] }, + { location: %i[latitude longitude timestamp type] }, + { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, + { phones: %i[phone type wa_id] }, { urls: [] }, + { name: %i[first_name formatted_name last_name] }] }]) end def error_params @@ -182,5 +196,11 @@ def fetch_message_from_twilio(twilio_message_sid) ErrorNotifier.report(e) nil end + + def handle_error(error) + exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: error['code'], message: error['title']) + ErrorNotifier.new(exception, context: { details: error['details'] }) + end end end +# rubocop:enable Metrics/ClassLength, Metrics/AbcSize From d7d7f1ebdecb9451098bd36cacef83dfbfd3a7ff Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 3 Aug 2023 14:47:17 +0200 Subject: [PATCH 16/57] Fix unsupported content logic --- .../three_sixty_dialog_inbound.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb index 2ad80148b..c0c9d1d76 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb @@ -110,11 +110,17 @@ def unsupported_content?(whats_app_message) message = whats_app_message[:messages].first return unless message + unsupported_content = message.keys.any? do |key| + UNSUPPORTED_CONTENT_TYPES.include?(key) + end errors = message[:errors] - ((errors && errors.first[:title].match?(/Unsupported message type/)) || errors.first[:title].match?(/Received Wrong Message Type/)) || - message.keys.any? do |key| - UNSUPPORTED_CONTENT_TYPES.include?(key) - end + return unsupported_content unless errors + + error_indicating_unsupported_content(errors) + end + + def error_indicating_unsupported_content(errors) + errors.first[:title].match?(/Unsupported message type/) || errors.first[:title].match?(/Received Wrong Message Type/) end def request_for_more_info?(text) From 25d1294ff0fcc9ef441973ebf286e0786d19d6a9 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 7 Aug 2023 13:21:08 +0200 Subject: [PATCH 17/57] Extract 360dialog to its own controller --- app/controllers/concerns/handle_callbacks.rb | 38 +++++++++ .../whats_app/onboarding_controller.rb | 1 - .../three_sixty_dialog_webhook_controller.rb | 81 +++++++++++++++++++ .../whats_app/webhook_controller.rb | 72 +---------------- config/routes.rb | 3 +- 5 files changed, 122 insertions(+), 73 deletions(-) create mode 100644 app/controllers/concerns/handle_callbacks.rb create mode 100644 app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb diff --git a/app/controllers/concerns/handle_callbacks.rb b/app/controllers/concerns/handle_callbacks.rb new file mode 100644 index 000000000..11e94135b --- /dev/null +++ b/app/controllers/concerns/handle_callbacks.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module HandleCallbacks + extend ActiveSupport::Concern + + private + + def handle_unknown_contributor(whats_app_phone_number) + exception = WhatsAppAdapter::UnknownContributorError.new(whats_app_phone_number: whats_app_phone_number) + ErrorNotifier.report(exception) + end + + def handle_request_for_more_info(contributor) + contributor.update!(whats_app_message_template_responded_at: Time.current) + + WhatsAppAdapter::Outbound.send_more_info_message!(contributor) + end + + def handle_unsubsribe_contributor(contributor) + contributor.update!(deactivated_at: Time.current) + + WhatsAppAdapter::Outbound.send_unsubsribed_successfully_message!(contributor) + ContributorMarkedInactive.with(contributor_id: contributor.id).deliver_later(User.all) + User.admin.find_each do |admin| + PostmarkAdapter::Outbound.contributor_marked_as_inactive!(admin, contributor) + end + end + + def handle_subscribe_contributor(contributor) + contributor.update!(deactivated_at: nil, whats_app_message_template_responded_at: Time.current) + + WhatsAppAdapter::Outbound.send_welcome_message!(contributor) + ContributorSubscribed.with(contributor_id: contributor.id).deliver_later(User.all) + User.admin.find_each do |admin| + PostmarkAdapter::Outbound.contributor_subscribed!(admin, contributor) + end + end +end diff --git a/app/controllers/whats_app/onboarding_controller.rb b/app/controllers/whats_app/onboarding_controller.rb index e9838e8fa..f9656b6b7 100644 --- a/app/controllers/whats_app/onboarding_controller.rb +++ b/app/controllers/whats_app/onboarding_controller.rb @@ -3,7 +3,6 @@ module WhatsApp class OnboardingController < ApplicationController def success - Rails.logger.debug params channel_ids = params[:channels].split('[').last.split(']').last.split(',') channel_ids.each do |channel_id| WhatsAppAdapter::CreateApiKey.perform_later(channel_id: channel_id) diff --git a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb new file mode 100644 index 000000000..49a4f4b05 --- /dev/null +++ b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module WhatsApp + class ThreeSixtyDialogWebhookController < ApplicationController + include HandleCallbacks + + skip_before_action :require_login, :verify_authenticity_token + + # rubocop:disable Metrics/AbcSize + def message + head :ok + return if params['statuses'].present? # TODO: Do we want to handle statuses? + + handle_error(params['errors']) if params['errors'].present? + + adapter = WhatsAppAdapter::ThreeSixtyDialogInbound.new + + adapter.on(WhatsAppAdapter::UNKNOWN_CONTRIBUTOR) do |whats_app_phone_number| + handle_unknown_contributor(whats_app_phone_number) + end + + adapter.on(WhatsAppAdapter::REQUEST_FOR_MORE_INFO) do |contributor| + handle_request_for_more_info(contributor) + end + + adapter.on(WhatsAppAdapter::REQUEST_TO_RECEIVE_MESSAGE) do |contributor| + handle_request_to_receive_message(contributor) + end + + adapter.on(WhatsAppAdapter::UNSUPPORTED_CONTENT) do |contributor| + WhatsAppAdapter::Outbound.send_unsupported_content_message!(contributor) + end + + adapter.on(WhatsAppAdapter::UNSUBSCRIBE_CONTRIBUTOR) do |contributor| + handle_unsubsribe_contributor(contributor) + end + + adapter.on(WhatsAppAdapter::SUBSCRIBE_CONTRIBUTOR) do |contributor| + handle_subscribe_contributor(contributor) + end + + adapter.consume(message_params.to_h) { |message| message.contributor.reply(adapter) } + end + # rubocop:enable Metrics/AbcSize + + private + + def message_params + params.permit({ three_sixty_dialog_webhook: [contacts: [:wa_id, { profile: [:name] }], + messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, + { button: [:text] }, { image: %i[id mime_type sha256] }, + { voice: %i[id mime_type sha256] }, + { video: %i[id mime_type sha256] }, + { errors: %i[code details title] }, + { location: %i[latitude longitude timestamp type] }, + { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, + { phones: %i[phone type wa_id] }, { urls: [] }, + { name: %i[first_name formatted_name last_name] }] }]] }, + contacts: [:wa_id, { profile: [:name] }], + messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, { button: [:text] }, + { image: %i[id mime_type sha256] }, { voice: %i[id mime_type sha256] }, + { video: %i[id mime_type sha256] }, + { errors: %i[code details title] }, + { location: %i[latitude longitude timestamp type] }, + { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, + { phones: %i[phone type wa_id] }, { urls: [] }, + { name: %i[first_name formatted_name last_name] }] }]) + end + + def handle_error(error) + exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: error['code'], message: error['title']) + ErrorNotifier.new(exception, context: { details: error['details'] }) + end + + def handle_request_to_receive_message(contributor) + contributor.update!(whats_app_message_template_responded_at: Time.current, whats_app_message_template_sent_at: nil) + + WhatsAppAdapter::Outbound.send!(contributor.received_messages.first) + end + end +end diff --git a/app/controllers/whats_app/webhook_controller.rb b/app/controllers/whats_app/webhook_controller.rb index 2845c4c77..cb63b51a2 100644 --- a/app/controllers/whats_app/webhook_controller.rb +++ b/app/controllers/whats_app/webhook_controller.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -# rubocop:disable Metrics/ClassLength, Metrics/AbcSize -# TODO: Separate WhatsApp implementations intro separate controllers module WhatsApp class WebhookController < ApplicationController + include HandleCallbacks + skip_before_action :require_login, :verify_authenticity_token UNSUCCESSFUL_DELIVERY = %w[undelivered failed].freeze @@ -38,41 +38,6 @@ def message adapter.consume(whats_app_message_params) { |message| message.contributor.reply(adapter) } end - def three_sixty_dialog_message - head :ok - return if params['statuses'].present? # TODO: Do we want to handle statuses? - - handle_error(params['errors']) if params['errors'].present? - - adapter = WhatsAppAdapter::ThreeSixtyDialogInbound.new - - adapter.on(WhatsAppAdapter::UNKNOWN_CONTRIBUTOR) do |whats_app_phone_number| - handle_unknown_contributor(whats_app_phone_number) - end - - adapter.on(WhatsAppAdapter::REQUEST_FOR_MORE_INFO) do |contributor| - handle_request_for_more_info(contributor) - end - - adapter.on(WhatsAppAdapter::REQUEST_TO_RECEIVE_MESSAGE) do |contributor| - handle_request_to_receive_message_three_sixty_dialog(contributor) - end - - adapter.on(WhatsAppAdapter::UNSUPPORTED_CONTENT) do |contributor| - WhatsAppAdapter::Outbound.send_unsupported_content_message!(contributor) - end - - adapter.on(WhatsAppAdapter::UNSUBSCRIBE_CONTRIBUTOR) do |contributor| - handle_unsubsribe_contributor(contributor) - end - - adapter.on(WhatsAppAdapter::SUBSCRIBE_CONTRIBUTOR) do |contributor| - handle_subscribe_contributor(contributor) - end - - adapter.consume(three_sixty_dialog_message_params.to_h) { |message| message.contributor.reply(adapter) } - end - def errors return unless error_params['Level'] == 'ERROR' @@ -105,27 +70,6 @@ def message_params :ReferralNumMedia, :SmsMessageSid, :SmsSid, :SmsStatus, :To, :WaId) end - def three_sixty_dialog_message_params - params.permit({ webhook: [contacts: [:wa_id, { profile: [:name] }], - messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, - { button: [:text] }, { image: %i[id mime_type sha256] }, { voice: %i[id mime_type sha256] }, - { video: %i[id mime_type sha256] }, - { errors: %i[code details title] }, - { location: %i[latitude longitude timestamp type] }, - { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, - { phones: %i[phone type wa_id] }, { urls: [] }, - { name: %i[first_name formatted_name last_name] }] }]] }, - contacts: [:wa_id, { profile: [:name] }], - messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, { button: [:text] }, - { image: %i[id mime_type sha256] }, { voice: %i[id mime_type sha256] }, - { video: %i[id mime_type sha256] }, - { errors: %i[code details title] }, - { location: %i[latitude longitude timestamp type] }, - { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, - { phones: %i[phone type wa_id] }, { urls: [] }, - { name: %i[first_name formatted_name last_name] }] }]) - end - def error_params params.permit(:AccountSid, :Level, :ParentAccountSid, :Payload, :PayloadType, :Sid, :Timestamp) end @@ -153,12 +97,6 @@ def handle_request_to_receive_message(contributor, twilio_message_sid) WhatsAppAdapter::Outbound.send!(message || contributor.received_messages.first) end - def handle_request_to_receive_message_three_sixty_dialog(contributor) - contributor.update!(whats_app_message_template_responded_at: Time.current, whats_app_message_template_sent_at: nil) - - WhatsAppAdapter::Outbound.send!(contributor.received_messages.first) - end - def handle_unsubsribe_contributor(contributor) contributor.update!(deactivated_at: Time.current) @@ -196,11 +134,5 @@ def fetch_message_from_twilio(twilio_message_sid) ErrorNotifier.report(e) nil end - - def handle_error(error) - exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: error['code'], message: error['title']) - ErrorNotifier.new(exception, context: { details: error['details'] }) - end end end -# rubocop:enable Metrics/ClassLength, Metrics/AbcSize diff --git a/config/routes.rb b/config/routes.rb index 8e95757d7..8268366ae 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -48,9 +48,8 @@ post '/webhook', to: 'webhook#message' post '/errors', to: 'webhook#errors' post '/status', to: 'webhook#status' - post '/three-sixty-dialog-status', to: 'webhook#three_sixty_dialog_status' get '/onboarding-successful', to: 'onboarding#success' - post '/three-sixty-dialog-webhook', to: 'webhook#three_sixty_dialog_message' + post '/three-sixty-dialog-webhook', to: 'three_sixty_dialog_webhook#message' end telegram_webhook Telegram::WebhookController From 9468a8650a33ac58c61b6133365a81fd6e450c27 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 7 Aug 2023 13:40:51 +0200 Subject: [PATCH 18/57] Handle successful client onboarding in webhook - set client id --- app/controllers/whats_app/onboarding_controller.rb | 12 ------------ .../three_sixty_dialog_webhook_controller.rb | 14 ++++++++++++++ app/models/setting.rb | 2 +- config/routes.rb | 2 +- 4 files changed, 16 insertions(+), 14 deletions(-) delete mode 100644 app/controllers/whats_app/onboarding_controller.rb diff --git a/app/controllers/whats_app/onboarding_controller.rb b/app/controllers/whats_app/onboarding_controller.rb deleted file mode 100644 index f9656b6b7..000000000 --- a/app/controllers/whats_app/onboarding_controller.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module WhatsApp - class OnboardingController < ApplicationController - def success - channel_ids = params[:channels].split('[').last.split(']').last.split(',') - channel_ids.each do |channel_id| - WhatsAppAdapter::CreateApiKey.perform_later(channel_id: channel_id) - end - end - end -end diff --git a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb index 49a4f4b05..30bbcea09 100644 --- a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb +++ b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb @@ -43,6 +43,16 @@ def message end # rubocop:enable Metrics/AbcSize + def create_api_key + channel_ids = create_api_key_params[:channels].split('[').last.split(']').last.split(',') + client_id = create_api_key_params[:client] + Setting.three_sixty_dialog_client_id = client_id + channel_ids.each do |channel_id| + WhatsAppAdapter::CreateApiKey.perform_later(channel_id: channel_id) + end + render 'onboarding/success' + end + private def message_params @@ -67,6 +77,10 @@ def message_params { name: %i[first_name formatted_name last_name] }] }]) end + def create_api_key_params + params.permit(:client, :channels, :revoked) + end + def handle_error(error) exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: error['code'], message: error['title']) ErrorNotifier.new(exception, context: { details: error['details'] }) diff --git a/app/models/setting.rb b/app/models/setting.rb index 10ca5af9a..3559f1f0c 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -79,7 +79,7 @@ def self.onboarding_hero=(blob) field :three_sixty_dialog_partner_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_REST_API_ENDPOINT', 'https://stoplight.io/mocks/360dialog/360dialog-partner-api/24588693') field :three_sixty_dialog_api_key - field :three_sixty_dialog_client_id, readonly: true, default: ENV.fetch('360_DIALOG_CLIENT_ID', nil) + field :three_sixty_dialog_client_id field :three_sixty_dialog_client_waba_account_id field :three_sixty_dialog_whats_app_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_WHATS_APP_REST_API_ENDPOINT', 'https://waba-sandbox.360dialog.io') diff --git a/config/routes.rb b/config/routes.rb index 8268366ae..fe4cf5af6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -48,7 +48,7 @@ post '/webhook', to: 'webhook#message' post '/errors', to: 'webhook#errors' post '/status', to: 'webhook#status' - get '/onboarding-successful', to: 'onboarding#success' + get '/onboarding-successful', to: 'three_sixty_dialog_webhook#create_api_key' post '/three-sixty-dialog-webhook', to: 'three_sixty_dialog_webhook#message' end From a8aae464c98a29d3b0f74a23d82b7f743f509581 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 7 Aug 2023 14:07:40 +0200 Subject: [PATCH 19/57] Set template namespace in create template job - namespace api_key setting in client --- app/adapters/whats_app_adapter/outbound.rb | 14 +++++++------- .../outbound/three_sixty_dialog_file.rb | 2 +- .../outbound/three_sixty_dialog_text.rb | 2 +- .../three_sixty_dialog_inbound.rb | 2 +- app/jobs/whats_app_adapter/create_api_key.rb | 4 ++-- app/jobs/whats_app_adapter/create_template.rb | 14 ++++++++++---- app/jobs/whats_app_adapter/set_webhook_url.rb | 4 ++-- app/jobs/whats_app_adapter/upload_file.rb | 4 ++-- app/models/setting.rb | 5 ++--- app/views/dashboard/index.html.erb | 2 +- 10 files changed, 29 insertions(+), 24 deletions(-) diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index 0f3f0112a..f320b3c58 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -19,7 +19,7 @@ def send_welcome_message!(contributor) return unless contributor_can_receive_messages?(contributor) welcome_message = I18n.t('adapter.whats_app.welcome_message', project_name: Setting.project_name) - if Setting.three_sixty_dialog_api_key.present? + if Setting.three_sixty_dialog_client_api_key.present? payload = if freeform_message_permitted?(contributor) text_payload(contributor, welcome_message) else @@ -36,7 +36,7 @@ def send_unsupported_content_message!(contributor) text = I18n.t('adapter.whats_app.unsupported_content_template', first_name: contributor.first_name, contact_person: contributor.organization.contact_person.name) - if Setting.three_sixty_dialog_api_key.present? + if Setting.three_sixty_dialog_client_api_key.present? WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) else WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) @@ -47,7 +47,7 @@ def send_more_info_message!(contributor) return unless contributor_can_receive_messages?(contributor) text = [Setting.about, "_#{I18n.t('adapter.whats_app.unsubscribe.instructions')}_"].join("\n\n") - if Setting.three_sixty_dialog_api_key.present? + if Setting.three_sixty_dialog_client_api_key.present? WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) else WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) @@ -58,7 +58,7 @@ def send_unsubsribed_successfully_message!(contributor) return unless contributor_can_receive_messages?(contributor) text = [I18n.t('adapter.whats_app.unsubscribe.successful'), "_#{I18n.t('adapter.whats_app.subscribe.instructions')}_"].join("\n\n") - if Setting.three_sixty_dialog_api_key.present? + if Setting.three_sixty_dialog_client_api_key.present? WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) else WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) @@ -98,7 +98,7 @@ def freeform_message_permitted?(recipient) def send_message_template(recipient, message) recipient.update(whats_app_message_template_sent_at: Time.current) - if Setting.three_sixty_dialog_api_key.present? + if Setting.three_sixty_dialog_client_api_key.present? WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: new_request_payload(recipient, message.request)) else text = I18n.t("adapter.whats_app.request_template.new_request_#{time_of_day}_#{rand(1..3)}", first_name: recipient.first_name, @@ -111,12 +111,12 @@ def send_message(recipient, message) files = message.files if files.blank? - if Setting.three_sixty_dialog_api_key.present? + if Setting.three_sixty_dialog_client_api_key.present? WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(recipient, message.text)) else WhatsAppAdapter::Outbound::Text.perform_later end - elsif Setting.three_sixty_dialog_api_key.present? + elsif Setting.three_sixty_dialog_client_api_key.present? files.each do |_file| WhatsAppAdapter::UploadFile.perform_later(message_id: message.id) end diff --git a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_file.rb b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_file.rb index c69287840..41f06647a 100644 --- a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_file.rb +++ b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_file.rb @@ -11,7 +11,7 @@ def perform(message_id:, file_id:) @file_id = file_id url = URI.parse("#{Setting.three_sixty_dialog_whats_app_rest_api_endpoint}/messages") - headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_api_key, 'Content-Type' => 'application/json' } + headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_client_api_key, 'Content-Type' => 'application/json' } request = Net::HTTP::Post.new(url.to_s, headers) request.body = payload.to_json response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| diff --git a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb index 8515da5f7..d0e4c5b4e 100644 --- a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb +++ b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb @@ -7,7 +7,7 @@ class ThreeSixtyDialogText < ApplicationJob def perform(payload:) url = URI.parse("#{Setting.three_sixty_dialog_whats_app_rest_api_endpoint}/messages") - headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_api_key, 'Content-Type' => 'application/json' } + headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_client_api_key, 'Content-Type' => 'application/json' } request = Net::HTTP::Post.new(url.to_s, headers) request.body = payload.to_json diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb index c0c9d1d76..721f34b9b 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb @@ -164,7 +164,7 @@ def create_message? def fetch_file(file_id) url = URI.parse("#{Setting.three_sixty_dialog_whats_app_rest_api_endpoint}/media/#{file_id}") - headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_api_key, 'Content-Type' => 'application/json' } + headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_client_api_key, 'Content-Type' => 'application/json' } request = Net::HTTP::Get.new(url.to_s, headers) response = Net::HTTP.start(url.host, url.port, use_ssl: true) do |http| http.request(request) diff --git a/app/jobs/whats_app_adapter/create_api_key.rb b/app/jobs/whats_app_adapter/create_api_key.rb index c110d0c58..4d6feb814 100644 --- a/app/jobs/whats_app_adapter/create_api_key.rb +++ b/app/jobs/whats_app_adapter/create_api_key.rb @@ -5,7 +5,7 @@ module WhatsAppAdapter class CreateApiKey < ApplicationJob def perform(channel_id:) - return if Setting.three_sixty_dialog_api_key.present? + return if Setting.three_sixty_dialog_client_api_key.present? @base_uri = Setting.three_sixty_dialog_partner_rest_api_endpoint @@ -52,7 +52,7 @@ def handle_response(response) case response.code.to_i when 200 api_key = JSON.parse(response.body)['api_key'] - Setting.three_sixty_dialog_api_key = api_key + Setting.three_sixty_dialog_client_api_key = api_key when 400..599 exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) ErrorNotifier.report(exception) diff --git a/app/jobs/whats_app_adapter/create_template.rb b/app/jobs/whats_app_adapter/create_template.rb index ff7825ab6..957515f7e 100644 --- a/app/jobs/whats_app_adapter/create_template.rb +++ b/app/jobs/whats_app_adapter/create_template.rb @@ -2,6 +2,7 @@ require 'net/http' +# rubocop:disable Metrics/ClassLength module WhatsAppAdapter class CreateTemplate < ApplicationJob def perform(template_name:, template_text:) @@ -14,7 +15,9 @@ def perform(template_name:, template_text:) @token = fetch_token unless token&.value && token.updated_at > 24.hours.ago @waba_account_id = Setting.three_sixty_dialog_client_waba_account_id - @waba_account_id = fetch_waba_account_id if waba_account_id.blank? + waba_accont_namespace = Setting.three_sixty_dialog_whats_app_template_namespace + @waba_account_id = fetch_client_info if waba_account_id.blank? || waba_accont_namespace.blank? + conditionally_create_template end @@ -75,7 +78,7 @@ def fetch_token Setting.three_sixty_dialog_partner_token = token end - def fetch_waba_account_id + def fetch_client_info url = URI.parse("#{base_uri}/partners/#{partner_id}/channels") headers = { Accept: 'application/json', @@ -88,8 +91,10 @@ def fetch_waba_account_id end channels_array = JSON.parse(response.body)['partner_channels'] client_hash = channels_array.find { |hash| hash['client']['id'] == Setting.three_sixty_dialog_client_id } - waba_account_id = client_hash['waba_account']['id'] - Setting.three_sixty_dialog_client_waba_account_id = waba_account_id + waba_account = client_hash['waba_account'] + Setting.three_sixty_dialog_whats_app_template_namespace = waba_account['namespace'] + + Setting.three_sixty_dialog_client_waba_account_id = waba_account['id'] end # rubocop:disable Metrics/MethodLength @@ -163,3 +168,4 @@ def handle_response(response) end end end +# rubocop:enable Metrics/ClassLength diff --git a/app/jobs/whats_app_adapter/set_webhook_url.rb b/app/jobs/whats_app_adapter/set_webhook_url.rb index b86167201..bd0cd58ba 100644 --- a/app/jobs/whats_app_adapter/set_webhook_url.rb +++ b/app/jobs/whats_app_adapter/set_webhook_url.rb @@ -5,11 +5,11 @@ module WhatsAppAdapter class SetWebhookUrl < ApplicationJob def perform - return unless Setting.three_sixty_dialog_api_key + return unless Setting.three_sixty_dialog_client_api_key base_uri = Setting.three_sixty_dialog_whats_app_rest_api_endpoint url = URI.parse("#{base_uri}/configs/webhook") - headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_api_key, 'Content-Type' => 'application/json' } + headers = { 'D360-API-KEY' => Setting.three_sixty_dialog_client_api_key, 'Content-Type' => 'application/json' } request = Net::HTTP::Post.new(url.to_s, headers) request.body = { url: "https://#{Setting.application_host}/whats_app/three-sixty-dialog-webhook" }.to_json diff --git a/app/jobs/whats_app_adapter/upload_file.rb b/app/jobs/whats_app_adapter/upload_file.rb index 3c704485d..1d682f5f3 100644 --- a/app/jobs/whats_app_adapter/upload_file.rb +++ b/app/jobs/whats_app_adapter/upload_file.rb @@ -5,7 +5,7 @@ module WhatsAppAdapter class UploadFile < ApplicationJob def perform(message_id:) - return if Setting.three_sixty_dialog_api_key.blank? + return if Setting.three_sixty_dialog_client_api_key.blank? @message_id = message_id message = Message.find(message_id) @@ -15,7 +15,7 @@ def perform(message_id:) base_uri = Setting.three_sixty_dialog_whats_app_rest_api_endpoint url = URI.parse("#{base_uri}/media") headers = { - 'D360-API-KEY': Setting.three_sixty_dialog_api_key, + 'D360-API-KEY': Setting.three_sixty_dialog_client_api_key, 'Content-Type': file.blob.content_type } request = Net::HTTP::Post.new(url.to_s, headers) diff --git a/app/models/setting.rb b/app/models/setting.rb index 3559f1f0c..214c918dc 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -78,13 +78,12 @@ def self.onboarding_hero=(blob) field :three_sixty_dialog_partner_password, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_PASSWORD', nil) field :three_sixty_dialog_partner_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_REST_API_ENDPOINT', 'https://stoplight.io/mocks/360dialog/360dialog-partner-api/24588693') - field :three_sixty_dialog_api_key + field :three_sixty_dialog_client_api_key field :three_sixty_dialog_client_id field :three_sixty_dialog_client_waba_account_id field :three_sixty_dialog_whats_app_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_WHATS_APP_REST_API_ENDPOINT', 'https://waba-sandbox.360dialog.io') - field :three_sixty_dialog_whats_app_template_namespace, readonly: true, - default: ENV.fetch('360_DIALOG_WHATS_APP_MESSAGE_TEMPLATE_NAMESPACE', nil) + field :three_sixty_dialog_whats_app_template_namespace field :inbound_email_password, readonly: true, default: ENV.fetch('RAILS_INBOUND_EMAIL_PASSWORD', nil) field :email_from_address, readonly: true, default: ENV['EMAIL_FROM_ADDRESS'] || 'redaktion@localhost' diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index efc69264b..8564f98d2 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -4,7 +4,7 @@ <%= t "dashboard.heading#{time_based_heading}", first_name: current_user.first_name %> <% end %> <%= c 'button', - link: "https://hub.360dialog.com/dashboard/app/#{ENV.fetch('360_DIALOG_PARTNER_ID', '')}/permissions?email=#{current_user.email}&name=#{current_user.name}&redirect_url=#{CGI.escape(whats_app_onboarding_successful_url)}", + link: "https://hub.360dialog.com/dashboard/app/#{ENV.fetch('360_DIALOG_PARTNER_ID', '')}/permissions?redirect_url=#{CGI.escape(whats_app_onboarding_successful_url)}", label: 'Connect to 360dialog', target: "_blank" %> From 8e4669313219ab2e37f8118ae5e27d51dab5ace9 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 7 Aug 2023 15:07:24 +0200 Subject: [PATCH 20/57] Remove duplicate controller methods --- .../whats_app/three_sixty_dialog_webhook_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb index 30bbcea09..6a333c1fe 100644 --- a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb +++ b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb @@ -28,7 +28,7 @@ def message end adapter.on(WhatsAppAdapter::UNSUPPORTED_CONTENT) do |contributor| - WhatsAppAdapter::Outbound.send_unsupported_content_message!(contributor) + WhatsAppAdapter::ThreeSixtyDialogOutbound.send_unsupported_content_message!(contributor) end adapter.on(WhatsAppAdapter::UNSUBSCRIBE_CONTRIBUTOR) do |contributor| @@ -89,7 +89,7 @@ def handle_error(error) def handle_request_to_receive_message(contributor) contributor.update!(whats_app_message_template_responded_at: Time.current, whats_app_message_template_sent_at: nil) - WhatsAppAdapter::Outbound.send!(contributor.received_messages.first) + WhatsAppAdapter::ThreeSixtyDialogOutbound.send!(contributor.received_messages.first) end end end From c0ed395fe143fcbcc542292d34483de9c975907c Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 7 Aug 2023 15:08:07 +0200 Subject: [PATCH 21/57] Decouple WhatsAppAdapter outbound by BSP - DRY out the code by separating the two implementations of WhatsApp by business solution provider --- app/adapters/whats_app_adapter/outbound.rb | 177 +----------------- .../three_sixty_dialog_outbound.rb | 165 ++++++++++++++++ .../whats_app_adapter/twilio_outbound.rb | 99 ++++++++++ .../whats_app/webhook_controller.rb | 30 +-- 4 files changed, 273 insertions(+), 198 deletions(-) create mode 100644 app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb create mode 100644 app/adapters/whats_app_adapter/twilio_outbound.rb diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index f320b3c58..edc6a160e 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -1,192 +1,29 @@ # frozen_string_literal: true -# rubocop:disable Metrics/ClassLength module WhatsAppAdapter class Outbound class << self def send!(message) - recipient = message&.recipient - return unless contributor_can_receive_messages?(recipient) - - if freeform_message_permitted?(recipient) - send_message(recipient, message) - else - send_message_template(recipient, message) - end + "WhatsAppAdapter::#{business_solution_provider}Outbound".constantize.send!(message) end def send_welcome_message!(contributor) - return unless contributor_can_receive_messages?(contributor) - - welcome_message = I18n.t('adapter.whats_app.welcome_message', project_name: Setting.project_name) - if Setting.three_sixty_dialog_client_api_key.present? - payload = if freeform_message_permitted?(contributor) - text_payload(contributor, welcome_message) - else - welcome_message_payload(contributor) - end - WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: payload) - else - WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: welcome_message) - end - end - - def send_unsupported_content_message!(contributor) - return unless contributor_can_receive_messages?(contributor) - - text = I18n.t('adapter.whats_app.unsupported_content_template', first_name: contributor.first_name, - contact_person: contributor.organization.contact_person.name) - if Setting.three_sixty_dialog_client_api_key.present? - WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) - else - WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) - end + "WhatsAppAdapter::#{business_solution_provider}Outbound".constantize.send_welcome_message!(contributor) end def send_more_info_message!(contributor) - return unless contributor_can_receive_messages?(contributor) - - text = [Setting.about, "_#{I18n.t('adapter.whats_app.unsubscribe.instructions')}_"].join("\n\n") - if Setting.three_sixty_dialog_client_api_key.present? - WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) - else - WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) - end + "WhatsAppAdapter::#{business_solution_provider}Outbound".constantize.send_more_info_message!(contributor) end def send_unsubsribed_successfully_message!(contributor) - return unless contributor_can_receive_messages?(contributor) - - text = [I18n.t('adapter.whats_app.unsubscribe.successful'), "_#{I18n.t('adapter.whats_app.subscribe.instructions')}_"].join("\n\n") - if Setting.three_sixty_dialog_client_api_key.present? - WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) - else - WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) - end - end - - def contributor_can_receive_messages?(recipient) - recipient&.whats_app_phone_number.present? - end - - def time_of_day - current_time = Time.current - morning = current_time.change(hour: 6) - day = current_time.change(hour: 11) - evening = current_time.change(hour: 17) - night = current_time.change(hour: 23) - - case current_time - when morning..day - 'morning' - when day..evening - 'day' - when evening..night - 'evening' - else - 'night' - end - end - - def freeform_message_permitted?(recipient) - responding_to_template_message = recipient.whats_app_message_template_responded_at.present? && - recipient.whats_app_message_template_responded_at > 24.hours.ago - latest_message_received_within_last_24_hours = recipient.replies.first&.created_at.present? && - recipient.replies.first.created_at > 24.hours.ago - responding_to_template_message || latest_message_received_within_last_24_hours + "WhatsAppAdapter::#{business_solution_provider}Outbound".constantize.send_unsubsribed_successfully_message!(contributor) end - def send_message_template(recipient, message) - recipient.update(whats_app_message_template_sent_at: Time.current) - if Setting.three_sixty_dialog_client_api_key.present? - WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: new_request_payload(recipient, message.request)) - else - text = I18n.t("adapter.whats_app.request_template.new_request_#{time_of_day}_#{rand(1..3)}", first_name: recipient.first_name, - request_title: message.request.title) - WhatsAppAdapter::Outbound::Text.perform_later(recipient: recipient, text: text) - end - end - - def send_message(recipient, message) - files = message.files - - if files.blank? - if Setting.three_sixty_dialog_client_api_key.present? - WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(recipient, message.text)) - else - WhatsAppAdapter::Outbound::Text.perform_later - end - elsif Setting.three_sixty_dialog_client_api_key.present? - files.each do |_file| - WhatsAppAdapter::UploadFile.perform_later(message_id: message.id) - end - else - files.each_with_index do |file, index| - WhatsAppAdapter::Outbound::File.perform_later(recipient: recipient, text: index.zero? ? message.text : '', file: file) - end - end - end - - # rubocop:disable Metrics/MethodLength - def new_request_payload(recipient, request) - { - recipient_type: 'individual', - to: recipient.whats_app_phone_number.split('+').last, - type: 'template', - template: { - namespace: Setting.three_sixty_dialog_whats_app_template_namespace, - language: { - policy: 'deterministic', - code: 'de' - }, - name: 'new_request_morning_1', # TODO: Use dynamic template name after WhatsAppAdapter::CreateTemplate works - components: [ - { - type: 'body', - parameters: [ - { - type: 'text', - text: recipient.first_name - }, - { - type: 'text', - text: request.title - } - ] - } - ] - } - } - end - # rubocop:enable Metrics/MethodLength - - def text_payload(recipient, text) - { - recipient_type: 'individual', - to: recipient.whats_app_phone_number.split('+').last, - type: 'text', - text: { - body: text - } - } - end + private - def welcome_message_payload(recipient) - { - recipient_type: 'individual', - to: recipient.whats_app_phone_number.split('+').last, - type: 'template', - template: { - namespace: template_namespace, - language: { - policy: 'deterministic', - code: 'de' - }, - name: 'welcome_message' - } - } + def business_solution_provider + Setting.three_sixty_dialog_client_api_key.present? ? 'ThreeSixtyDialog' : 'Twitter' end end end end -# rubocop:enable Metrics/ClassLength diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb new file mode 100644 index 000000000..ece9e6fc8 --- /dev/null +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +module WhatsAppAdapter + class ThreeSixtyDialogOutbound + class << self + include TimeBasedTemplates + + def send!(message) + recipient = message&.recipient + return unless contributor_can_receive_messages?(recipient) + + if freeform_message_permitted?(recipient) + send_message(recipient, message) + else + send_message_template(recipient, message) + end + end + + def send_welcome_message!(contributor) + return unless contributor_can_receive_messages?(contributor) + + welcome_message = I18n.t('adapter.whats_app.welcome_message', project_name: Setting.project_name) + payload = if freeform_message_permitted?(contributor) + text_payload(contributor, welcome_message) + else + welcome_message_payload(contributor) + end + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: payload) + end + + def send_unsupported_content_message!(contributor) + return unless contributor_can_receive_messages?(contributor) + + text = I18n.t('adapter.whats_app.unsupported_content_template', first_name: contributor.first_name, + contact_person: contributor.organization.contact_person.name) + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) + end + + def send_more_info_message!(contributor) + return unless contributor_can_receive_messages?(contributor) + + text = [Setting.about, "_#{I18n.t('adapter.whats_app.unsubscribe.instructions')}_"].join("\n\n") + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) + end + + def send_unsubsribed_successfully_message!(contributor) + return unless contributor_can_receive_messages?(contributor) + + text = [I18n.t('adapter.whats_app.unsubscribe.successful'), + "_#{I18n.t('adapter.whats_app.subscribe.instructions')}_"].join("\n\n") + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(contributor, text)) + end + + private + + def contributor_can_receive_messages?(recipient) + recipient&.whats_app_phone_number.present? + end + + def time_of_day + current_time = Time.current + morning = current_time.change(hour: 6) + day = current_time.change(hour: 11) + evening = current_time.change(hour: 17) + night = current_time.change(hour: 23) + + case current_time + when morning..day + 'morning' + when day..evening + 'day' + when evening..night + 'evening' + else + 'night' + end + end + + def freeform_message_permitted?(recipient) + responding_to_template_message = recipient.whats_app_message_template_responded_at.present? && + recipient.whats_app_message_template_responded_at > 24.hours.ago + latest_message_received_within_last_24_hours = recipient.replies.first&.created_at.present? && + recipient.replies.first.created_at > 24.hours.ago + responding_to_template_message || latest_message_received_within_last_24_hours + end + + def send_message_template(recipient, message) + recipient.update(whats_app_message_template_sent_at: Time.current) + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: new_request_payload(recipient, message.request)) + end + + def send_message(recipient, message) + files = message.files + + if files.blank? + WhatsAppAdapter::Outbound::ThreeSixtyDialogText.perform_later(payload: text_payload(recipient, message.text)) + else + files.each do |_file| + WhatsAppAdapter::UploadFile.perform_later(message_id: message.id) + end + end + end + + # rubocop:disable Metrics/MethodLength + def new_request_payload(recipient, request) + { + recipient_type: 'individual', + to: recipient.whats_app_phone_number.split('+').last, + type: 'template', + template: { + namespace: Setting.three_sixty_dialog_whats_app_template_namespace, + language: { + policy: 'deterministic', + code: 'de' + }, + name: 'new_request_morning_1', # TODO: Use dynamic template name after WhatsAppAdapter::CreateTemplate works + components: [ + { + type: 'body', + parameters: [ + { + type: 'text', + text: recipient.first_name + }, + { + type: 'text', + text: request.title + } + ] + } + ] + } + } + end + # rubocop:enable Metrics/MethodLength + + def text_payload(recipient, text) + { + recipient_type: 'individual', + to: recipient.whats_app_phone_number.split('+').last, + type: 'text', + text: { + body: text + } + } + end + + def welcome_message_payload(recipient) + { + recipient_type: 'individual', + to: recipient.whats_app_phone_number.split('+').last, + type: 'template', + template: { + namespace: template_namespace, + language: { + policy: 'deterministic', + code: 'de' + }, + name: 'welcome_message' + } + } + end + end + end +end diff --git a/app/adapters/whats_app_adapter/twilio_outbound.rb b/app/adapters/whats_app_adapter/twilio_outbound.rb new file mode 100644 index 000000000..d83188663 --- /dev/null +++ b/app/adapters/whats_app_adapter/twilio_outbound.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module WhatsAppAdapter + class TwilioOutbound + class << self + def send!(message) + recipient = message&.recipient + return unless contributor_can_receive_messages?(recipient) + + if freeform_message_permitted?(recipient) + send_message(recipient, message) + else + send_message_template(recipient, message) + end + end + + def send_welcome_message!(contributor) + return unless contributor_can_receive_messages?(contributor) + + welcome_message = I18n.t('adapter.whats_app.welcome_message', project_name: Setting.project_name) + WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: welcome_message) + end + + def send_unsupported_content_message!(contributor) + return unless contributor_can_receive_messages?(contributor) + + text = I18n.t('adapter.whats_app.unsupported_content_template', first_name: contributor.first_name, + contact_person: contributor.organization.contact_person.name) + WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) + end + + def send_more_info_message!(contributor) + return unless contributor_can_receive_messages?(contributor) + + text = [Setting.about, "_#{I18n.t('adapter.whats_app.unsubscribe.instructions')}_"].join("\n\n") + WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) + end + + def send_unsubsribed_successfully_message!(contributor) + return unless contributor_can_receive_messages?(contributor) + + text = [I18n.t('adapter.whats_app.unsubscribe.successful'), "_#{I18n.t('adapter.whats_app.subscribe.instructions')}_"].join("\n\n") + WhatsAppAdapter::Outbound::Text.perform_later(recipient: contributor, text: text) + end + + private + + def contributor_can_receive_messages?(recipient) + recipient&.whats_app_phone_number.present? + end + + def time_of_day + current_time = Time.current + morning = current_time.change(hour: 6) + day = current_time.change(hour: 11) + evening = current_time.change(hour: 17) + night = current_time.change(hour: 23) + + case current_time + when morning..day + 'morning' + when day..evening + 'day' + when evening..night + 'evening' + else + 'night' + end + end + + def freeform_message_permitted?(recipient) + responding_to_template_message = recipient.whats_app_message_template_responded_at.present? && + recipient.whats_app_message_template_responded_at > 24.hours.ago + latest_message_received_within_last_24_hours = recipient.replies.first&.created_at.present? && + recipient.replies.first.created_at > 24.hours.ago + responding_to_template_message || latest_message_received_within_last_24_hours + end + + def send_message_template(recipient, message) + recipient.update(whats_app_message_template_sent_at: Time.current) + text = I18n.t("adapter.whats_app.request_template.new_request_#{time_of_day}_#{rand(1..3)}", first_name: recipient.first_name, + request_title: message.request.title) + WhatsAppAdapter::Outbound::Text.perform_later(recipient: recipient, text: text) + end + + def send_message(recipient, message) + files = message.files + + if files.blank? + WhatsAppAdapter::Outbound::Text.perform_later + else + files.each_with_index do |file, index| + WhatsAppAdapter::Outbound::File.perform_later(recipient: recipient, text: index.zero? ? message.text : '', file: file) + end + end + end + end + end +end diff --git a/app/controllers/whats_app/webhook_controller.rb b/app/controllers/whats_app/webhook_controller.rb index cb63b51a2..93d038c77 100644 --- a/app/controllers/whats_app/webhook_controller.rb +++ b/app/controllers/whats_app/webhook_controller.rb @@ -23,7 +23,7 @@ def message end adapter.on(WhatsAppAdapter::UNSUPPORTED_CONTENT) do |contributor| - WhatsAppAdapter::Outbound.send_unsupported_content_message!(contributor) + WhatsAppAdapter::TwilioOutbound.send_unsupported_content_message!(contributor) end adapter.on(WhatsAppAdapter::UNSUBSCRIBE_CONTRIBUTOR) do |contributor| @@ -84,37 +84,11 @@ def handle_unknown_contributor(whats_app_phone_number) ErrorNotifier.report(exception) end - def handle_request_for_more_info(contributor) - contributor.update!(whats_app_message_template_responded_at: Time.current) - - WhatsAppAdapter::Outbound.send_more_info_message!(contributor) - end - def handle_request_to_receive_message(contributor, twilio_message_sid) contributor.update!(whats_app_message_template_responded_at: Time.current, whats_app_message_template_sent_at: nil) message = (send_requested_message(contributor, twilio_message_sid) if twilio_message_sid) - WhatsAppAdapter::Outbound.send!(message || contributor.received_messages.first) - end - - def handle_unsubsribe_contributor(contributor) - contributor.update!(deactivated_at: Time.current) - - WhatsAppAdapter::Outbound.send_unsubsribed_successfully_message!(contributor) - ContributorMarkedInactive.with(contributor_id: contributor.id).deliver_later(User.all) - User.admin.find_each do |admin| - PostmarkAdapter::Outbound.contributor_marked_as_inactive!(admin, contributor) - end - end - - def handle_subscribe_contributor(contributor) - contributor.update!(deactivated_at: nil, whats_app_message_template_responded_at: Time.current) - - WhatsAppAdapter::Outbound.send_welcome_message!(contributor) - ContributorSubscribed.with(contributor_id: contributor.id).deliver_later(User.all) - User.admin.find_each do |admin| - PostmarkAdapter::Outbound.contributor_subscribed!(admin, contributor) - end + WhatsAppAdapter::TwilioOutbound.send!(message || contributor.received_messages.first) end def send_requested_message(contributor, twilio_message_sid) From 8c2ba7c362784322cbf0c4edb4404e8dcb1d1dab Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 7 Aug 2023 15:46:44 +0200 Subject: [PATCH 22/57] Add env to ansible/docker --- ansible/roles/installation/tasks/main.yml | 4 ++++ docker-compose.prod.yml | 4 ++++ docker-compose.yml | 2 ++ 3 files changed, 10 insertions(+) diff --git a/ansible/roles/installation/tasks/main.yml b/ansible/roles/installation/tasks/main.yml index ac8f28f7e..6df9b2b9f 100644 --- a/ansible/roles/installation/tasks/main.yml +++ b/ansible/roles/installation/tasks/main.yml @@ -53,6 +53,10 @@ TWILIO_AUTH_TOKEN: "{{ rails.twilio.auth_token }}" TWILIO_API_KEY_SID: "{{ rails.twilio.api_key.sid }}" TWILIO_API_KEY_SECRET: "{{ rails.twilio.api_key.secret }}" + 360_DIALOG_PARTNER_ID: "{{ rails.three_sixty_dialog.partner.id }}" + 360_DIALOG_PARTNER_USERNAME: "{{ rails.three_sixty_dialog.partner.username }}" + 360_DIALOG_PARTNER_PASSWORD: "{{ rails.three_sixty_dialog.partner.password }}" + community.general.docker_compose: project_src: /home/ansible build: no diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0b4c6e301..56c9b5c98 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -31,6 +31,10 @@ x-prod-defaults: &x-prod-defaults TWILIO_AUTH_TOKEN: "${TWILIO_AUTH_TOKEN}" TWILIO_API_KEY_SID: "${TWILIO_API_KEY_SID}" TWILIO_API_KEY_SECRET: "${TWILIO_API_KEY_SECRET}" + 360_DIALOG_PARTNER_ID: "${360_DIALOG_PARTNER_ID}" + 360_DIALOG_PARTNER_USERNAME: "${360_DIALOG_PARTNER_USERNAME}" + 360_DIALOG_PARTNER_PASSWORD: "${360_DIALOG_PARTNER_PASSWORD}" + volumes: - ./log:/app/log - ./storage:/app/storage diff --git a/docker-compose.yml b/docker-compose.yml index 425ec2b56..1916ef568 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ x-defaults: &x-defaults POSTGRES_PORT: "${POSTGRES_PORT:-5432}" SIGNAL_CLI_REST_API_ENDPOINT: "http://signal:8080" SIGNAL_CLI_REST_API_ATTACHMENT_PATH: "signal-cli-config/attachments/" + 360_DIALOG_PARTNER_REST_API_ENDPOINT: "https://hub.360dialog.io/api/v2" + 360_DIALOG_WHATS_APP_REST_API_ENDPOINT: "https://waba.360dialog.io/v1" services: app: From 4a675721d166df10c00389e054eea58f7d316106 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 8 Aug 2023 20:33:06 +0200 Subject: [PATCH 23/57] Add env variables to ansible config for staging --- .../inventories/staging/host_vars/staging.yml | 224 +++++++++--------- 1 file changed, 115 insertions(+), 109 deletions(-) diff --git a/ansible/inventories/staging/host_vars/staging.yml b/ansible/inventories/staging/host_vars/staging.yml index c36965b57..b96b0fb4e 100644 --- a/ansible/inventories/staging/host_vars/staging.yml +++ b/ansible/inventories/staging/host_vars/staging.yml @@ -1,110 +1,116 @@ $ANSIBLE_VAULT;1.1;AES256 -38376265303534616566333137376366636462643532666464356163663936396665366263373663 -6535383735653737306639383636626632333663303733610a613536323430363638366466353737 -61633038623236316139663361393439316363306165353731333763663365363366333838616666 -6339323466636539350a356331613664646435306134613030346238636437663430383335343835 -64303132366233643336656331393731633734333334616639353337643533323763626331623864 -36306139663234353537633966623037653935616364643762316465356438373434366637613334 -37616666353539363930306563353066636361326562336130626465316262313935656535643331 -30653562386166303137383966393136343864306266306164623564616463303566313936643038 -35616130623463393137616635666236643562656231363436393763313233633166323533633564 -34623663366130353231656136323735616561643331633263366537646662366563626136626566 -38346364393338313931306432356131303665656437623662656663316232616238396334333430 -36356532633131353534383937326262633033663461613739323662343264363761656137326538 -62663439653666386163393931326639623961303064613636363261616362633661326439353765 -33616432383363343337316530623136353062333366343934343430373434626563646561636138 -36323632376230333165663138383731643136376662356665353538343335613237326666656532 -36336331393139333366396165656436306662643163333138353263633232626439323034373635 -63666532666531356233653165346132363133356566323163623362323564633136316333613637 -62343566333565316363393332666434643033356435326431646365383464393563363961383664 -37336166326530636530323232383831373732623365393861616433643830646533396236323663 -61333863343039653731303130323366653231383065393734616363663236666232373936636265 -65343136653265623766663935353434623330306332346437393637643762333939373939386665 -39663338653436333538313039646165306363383764373135626139373736383134646235666435 -39303864396266356533366639346332666366656131323733313366393166396666653364653962 -61353035376130653630363630613334353636373462333330373631356139393038376536633565 -36646463646561363030366231306633333633333766366339376537323661626436343338633864 -64303234626236636565633961643831323466383164346665313135346236616237613163643463 -31633438653639366661363537663937633839306666623336323332383533303464323731313933 -64653533303863646530636536306337383333656335323237356464346233623263393633353463 -33303531623037636636343036613065653566366666616535366263326632336666633566356538 -39623664343662303264303931653462366537323665653236376335386235653766306131366564 -66336565653839616537376330613563626335326230653530643238626337313162626565323532 -31613363666230393931343137336462653937643739343664613461616134663364313837623832 -35336339663733353330326634653061646238316664646631666132343661323933336230363762 -63643765383934366536626364373934326436353865613938306333663363303236393565663762 -30613161316133636261633930313738636332373838633564363436383762366537623430643233 -64346265323664373336383836643833663235353134363463373836646631393931396665386139 -39316232393438326433326338393666386432613264626564303064346537333934343232303436 -63623139653135643338366235613434613062316139653363343839616336633730376634306133 -65316637326235643061363730636163653439623939613038623033623664363737313166356464 -62653437626237306164313531386465656530613335393637666437663534643464396434303165 -39656236303532303334333331333738363863353261633136363465353261356536633739383837 -33366438316530393539396637353731396363303839336462623739643239663265366662353336 -64643232373762393434306438663938393433343136303237363163393834343738333830346333 -31386531396333313261326261333333333363323630383565343965363833656265623430396239 -36613434383865663366363037643265323832363663396230356431383463633139643733323336 -35623033366236333664633630333636316338643630323034626535633733306266663162303962 -33353565303730613739623461323831323130333831386331643530366635343962613339663039 -65636563643363383232643232666635396336326162653061366639626265393830653363613762 -31613230303839653633303165393939313234653430393134343062346162376331313831363335 -61333232373063303938336566346432303433666364386238343632613061326134643531643531 -31326562336239386530373066643235306139626231353239326535393033666330646665396132 -35653063386236626133316233386365376662313061373966616231356138663838646533393239 -30363333666562313637663063326632643564383762646132343332613333356663363964363033 -38376634303761663363323830336264333064316665313261646330376333396538376438653265 -38666432633735613933386162633139666330623062616636613135313065393135343538633134 -65323862653364663765643737323932393338333239393564613664393463626633356563653561 -33376435313533666261323666303862636435653761636263633538616661346135323339656636 -66366635326631386162316134373330323032363237633164386431626437396264616435333735 -35316436333434343333363832643530353537646534373064626137636234323162363531663764 -35326464303735323036343530643639666133623832303038333565633064353932336435636463 -63303861383534316132623138633963326265666330306262353735373961633037376135306264 -32616231623239663732323565613035323334396566303431653739623163663236303331366234 -34646131613563306635316237636334633437393733363137336230346335653733613838393234 -31326135326365663635653638323363366539643139623738353361623534663563373264336136 -37633564353935353765666665316164383566666538323934383334303732303430353131303934 -63376564653164336433383935663136346166666530663137623563643165383933646430346637 -63363533306132663066383766663633396135663064313732386131396665623334653362383135 -61383161386430363239383966643439616165613662636430303734656665623237663936626635 -37383638326266636231356462643638373061623162613832343739613265613735653130353565 -62363738386339343132383435396632383663656338333636326335616164356239643736383565 -64306538336337366661323164326235363037663537333338326561333636623862393333373535 -61636462653436356163383331313766653661633038303161323239633937306462306465616638 -33633761623566663935653135393030613063646638356235666438326465376231366533383036 -35396639353530613261616364656534306662373861613130373263623735343966303666396631 -64323039643365393662323232373562636264643066383934333865326638656637616366613362 -31666664313037373338626237383332626431353434356665326361383734613566363934303561 -34316462636530633562663661396135656335656464353134363237636633376136343439346262 -30373962383164643336393263666236373132356235653133663264333036383431333966373631 -39643130316135353063643333393130303833333131393662623139306333613338656638393438 -36626433336434393139653338623434376333396235383930633234626362663534366439363364 -34346263383733396536656635376439643538383062303236646137623166353663656438356330 -35356362633437643733363063353738653666396631366662636466663264383463343863613738 -61393536643134623338346465636534323930306432616462323632643039326536306135393531 -62353438323463343065363131383763366264613136336138383164613134646131663136313764 -39336162613361613866653936616166313366613838616164346336626339303465356132636466 -31646237386335636161653830383164353761666464636661373036336332363031633465663931 -62346134636162303432633964313235336563656432663831313832656138353335373830656161 -66343465373537356230393834316464326363366133313639306166646364313734393334346363 -66643837636465616563396331323130383665653636383264353936613164303034356530303361 -39643334623836633561386636323838343839623961643364616135393863326536353338303431 -61666264356433613365646466623535323533663730373431383266383965343939373366396631 -62393262316166343264623636613632653135353663393631333533613330303735613734616434 -36633133643339396134663232353334643732396632613838653437356137356134663732333837 -64656632613234353133633535653065383763393564666665653261363864616430636537356231 -30623661613566333833323835643233626636386334366565616261323730323763353966333034 -30386562353366343832346536316661343131353661613366303566653837313663383435356330 -36313733393134376463363339643939353237313263356533633236343166623232623064386539 -33653533326335643662353731376137343162343532356333626532313635663932656261653536 -39623936393066373130393662323432616537633463313964373631393938376336643830333864 -32396135383335386665653038373638633966316164656664353431663138623866383432306438 -39323834353838356265613838613065626231616534326335303832623733373737363764333062 -64393264396439626630313762663432376331346230303032666538316336313665363136303961 -39656332653263626336396339303331663232636633343332363535393533613430306561393738 -63363430303332636433663463316433653533323464326436666665663834323035336235333461 -37396264666132666337336130616438623162366333626365383366343233653135303366343133 -39653734643933336131393065333762386633366262396335626239666161626432313265353837 -63613233343039643166393430386262343964653265646631343732313639633961653061313236 -36623361613231656238636230366566646263633934323161336163303831363732303535303036 -3861 +62623537373466336432333137376566643835306139633135656165356238366561333830663965 +6530383633656662313733366166633064316139333837390a653765383635356162303865323831 +61316466396562323463643930366362613530373536393965306137366335646463316464656166 +3431343261313562390a343432383330646234666663396135343138663766326634363533363739 +37383765373932356431653936336639363734316139393765326261643265666163613262643563 +33316431353036346130613263653430366337396332623132663536303666396662636136646533 +32393038333430626536363732373066313066333137616162313839636431396536366130316266 +31306363366234376333613739303134326564633162316265616237336564633431323139373531 +64343836356236323232343434316436396561303136616663323164353861633631353639643739 +61396130643466643365643030303232333964353737613465376537326330653131323461623734 +61646534626262323637336564393630656666373834643932326564366565343631306133623564 +33366461376431306364653439653566663665646532303439376430643262643639643731396235 +66616565666430393464656539666261376235383562613631346638393435363266313430383365 +34353130383938386566366434363537363465663434386537376335363061376166303032323635 +61363038346230623162346564653465383332303662633537393933376639626231343965333764 +61323662643630666662336635616638343163343933393331616130353533323434366661366538 +35316537643732626334653566613039323961353632616264386666383836366166646130663666 +64306130363234383134373663613235373135323231306662653339386664326231353330633532 +62343865333131343266393431323739663439343533336436626263633865343332316439366165 +35383666313939303766346433356262323933396336656539343739363835353332653166666633 +34643466633132376333313634326261333862663332623262353938666263353138643638306534 +31613765353835366365373835336566623464636463363365323563353662346535643132343462 +35356233636565393064313463643831353665376261353938383564626530623534303837613663 +64663364376134383562313938386363666539306638616530333530623538373263643232323761 +35313434396338303139343536643836363837636663366133633935326632393039616336333263 +34333836363432313139636463646136633064633030343033303861333432303637393433353132 +30616561303931333736326136386534616636633330373866646465356462363437663936353635 +65326139636362383737386361626463386262323034393332306433353962353631643865326231 +65373865333131333132633534373237623632656136313731643064666435653961363837303034 +63663533306539393463363062343065313138626338316233306165373766313965376535306239 +61306663373334353739636264383765393133353236613433613234666562626234646530623562 +30343963323761326535386264343230326532303938363265333631346164633937366530346264 +63346236356566306564333939333864333965663865366131313430396130663266666436623032 +66646162396135636562343539383161363436633763363035306430343034316430366532366136 +62383939393064316536636634333631613631373432303438363836353966373963653864343933 +38663662373939356365356633653739393962646535646439343765656366303934623939396661 +37373134376330343362663830323936626562333462613232613363386239613866623733333837 +30393736616232316233656338663835316364643866323035343939343236306236323234626331 +62636633613964343132636536396233393662393963646236333936623937383866366138323338 +36626231663938666337366135333761623666326263393636326563333335633735373032353336 +31646337323737633337303637313235386132303432653033616333393131396131616333376564 +66633064333533323633623239353934613166323939633339393530343737353239343964666232 +39663163353665653834666537316232613737636232343566306137326265316135636664323134 +31656363646137363065623666633965306334373538313431306435346536303830353134316333 +36346537333436353034623133646266386234663964653063663538363638643130663262343938 +38353731613038366437323133633766386539363332303764653135336138376139333636653830 +36383435663732353862623066343438383965323335353537633766386239313464306339383765 +34376134303336363035376632353336303865383062343132316133626139366138386331613430 +38366237623862333462666535623566386366656533363735653833623663383863336236663662 +34313065663538303539663837343230396135626462653064386533313264346632613938646465 +62636130313433643633633532356437386131393961666436303461623462663036646235363033 +33383539653532393938643537303565306232306134623931626233633634343637616236643563 +32666436636639303831623562363738383032386433346562636230376634343964306635623532 +65346662303266623361323666313234326666613134643634633831363266643338633135343331 +32346130306436653638663962383464643130386366343534346562643538383362393935303063 +30623836626663373934316330376461383066336334663132376133343337663338646635646130 +36303136393136356431373838343561336461313236376166346637323337363861616230663861 +39376462376330326632353063303566386364383633663232356439373962303339373763386533 +61633664303631313432613563646465616235356537616637613231363262303635353033306166 +64636361623061366234643666323232633565393263383961333633316139353665383532616465 +37313361666566646430363539643961653666353532653334353264613464336362663863323132 +66393362373538663836623066626136393832353163393935303936653331396432366230656662 +33646561316439653631643861333064376131376535623037393336653838613439353936396531 +37636664343138363532386633333234663262376262363665336339656466616630363064643366 +33373236326565363766363038663361386566326464666164666337363434613639663538323731 +36373636656565663538633032366330303163643336623561363134313130333036633436363365 +35633337636164313064323963383166313865643136303766623765613030623130636634636437 +64356439633033613630396164393936303634343532316362306535333934333563343638353639 +66653639363235623038616536646266393534386135663933626439363765636161313163646262 +36303636663633636630633861323739336135663334326138343134393065353039303030643933 +30316265343666356439343437373338656165313439636532636163373733623137376436643263 +31656431633239633235333231663866336439636661316338663662663131343135386235383537 +37623234653235303164623236353766326638316638343236616537343765336264376539663633 +35313735626334313633376466376265376533303937373636643837323532396365623563343066 +39383563636365306432366530316362653937643131356362346338366661353662313665633462 +36613436363764333233303639653833303238386164323130386332316264636137366334633162 +39363034356561336663333564663966336435616261363932643632666135386362663331623862 +62366438306362613537323232326537643932326236663665626666303438636130613131363464 +62656435353038393936393064616237303765303465353532386131663065366134303234343365 +65343463633963303730376535303737336139343166643939353333663962613733393938623365 +61643936356461376165376131353664623964336333303365653138666663386162376266656536 +65396334613363306331313466643433646439393137363334653039663232336436666265343639 +31386433636533366233313236343035626437373834313332613239303563363264326162306434 +64386639653163633837633030316234316538646536353638363862393339326132623932663933 +33626636643763383337623566343366626265633066613632306161383639353439653133663963 +31663138313439316665656532643034653934323337323836373763353031613934363562303838 +65666335303038643461363562383333376464303334386237613864643132646531303638323631 +63353832633934363031373333313438373033313266643962393961373838636665336161633134 +63323463653231306136653233356638383639653531373439623464313961393861363635636364 +36373237363361333739366236306439303561333639346262653235653965316438656636373064 +30313263623534376232366164313034333264316564386336653631303966623336336530633634 +63316534623863323331636137376637306666613039313137633637613465363038643062373866 +36666662353033373939376438623664356135626363393230643135636333663131363235666665 +62663030633265393934346138663166363634353232386463363161343230356232626437366331 +30623433353465663066353964643835333235626130663763386239373861323930663336323333 +64666333396466313839666337303036313533383236363434336430373732323138326335356437 +64356235653436353163363632356635363739333962376263343435326265323039336537666132 +63646435373730356663313963383336336437376430666562376265623538623361666236353430 +63323465633362353331643034333230386335663966366261353631383434303739313664303037 +66306366653761386434373534643038363964616230646439623061356534333232653539343235 +66336639643536386666346631626230393431666437623731663238636431353065313131376463 +37343534303164653637386532666233313334306134646339373264626430663335626536626232 +61363065623037376266663236383365653336346435313632353366616562333938303630306236 +33656636306532363833633731343435623136353533636330356539343030646266356465356236 +38356531396138653232356133366635323136323437396535626164316538356134626235396137 +32626566386165323438643831393639346237616362346164393939643161396532356363633639 +35333931613365323562306466626166643237343563393032623261663935313832313235613939 +33613033343361346230623931363139356139303132343637356631303964663663343335306636 +66356664313362393436386632336461336638623665336530393863633132306130666634663061 +62666339663439653235666439663737396465356637616432346230616134336462376133313039 +33636435336430376235333737336366303138363739613566653339643630323335336532323430 +64346538613430663837316333636561306536363539653233623761643938663334616435343161 +31306466646334636264353364643838376336633262343062383264623936666365616437386533 +32316233373034313662386365313934636235316338633636393734633237613366643161313036 +316638316333303463376332363662363639 From d7cd83e2d9374584b3f6d0030ecaefb1d22847a0 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 8 Aug 2023 20:34:50 +0200 Subject: [PATCH 24/57] Fix syntax errors --- app/adapters/whats_app_adapter/outbound.rb | 2 +- app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index edc6a160e..abc03b47b 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -22,7 +22,7 @@ def send_unsubsribed_successfully_message!(contributor) private def business_solution_provider - Setting.three_sixty_dialog_client_api_key.present? ? 'ThreeSixtyDialog' : 'Twitter' + Setting.three_sixty_dialog_client_api_key.present? ? 'ThreeSixtyDialog' : 'Twilio' end end end diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb index ece9e6fc8..5f579045b 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb @@ -3,8 +3,6 @@ module WhatsAppAdapter class ThreeSixtyDialogOutbound class << self - include TimeBasedTemplates - def send!(message) recipient = message&.recipient return unless contributor_can_receive_messages?(recipient) From cd774668ffaad89495f445c0c9a6d3751c1d4b4b Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 8 Aug 2023 20:35:54 +0200 Subject: [PATCH 25/57] Fix naming issues with numerical 360 --- .env.template | 12 +++++------- ansible/roles/installation/tasks/main.yml | 6 +++--- app/models/setting.rb | 11 ++++++----- app/views/dashboard/index.html.erb | 2 +- docker-compose.prod.yml | 6 +++--- docker-compose.yml | 4 ++-- 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.env.template b/.env.template index 98f560594..f360e9538 100644 --- a/.env.template +++ b/.env.template @@ -25,10 +25,8 @@ THREEMARB_PRIVATE= # DOCKER_IMAGE_TAG= # SENTRY_DSN= # -# 360_DIALOG_ACCESS_TOKEN= -# 360_DIALOG_PARTNER_ID= -# 360_DIALOG_PARTNER_USERNAME= -# 360_DIALOG_PARTNER_PASSWORD= -# 360_DIALOG_WHATS_APP_MESSAGE_TEMPLATE_NAMESPACE= -# 360_DIALOG_WHATS_APP_REST_API_ENDPOINT= -# 360_DIALOG_PARTNER_REST_API_ENDPOINT= \ No newline at end of file +# THREE_SIXTY_DIALOG_PARTNER_ID= +# THREE_SIXTY_DIALOG_PARTNER_USERNAME= +# THREE_SIXTY_DIALOG_PARTNER_PASSWORD= +# THREE_SIXTY_DIALOG_WHATS_APP_REST_API_ENDPOINT= +# THREE_SIXTY_DIALOG_PARTNER_REST_API_ENDPOINT= \ No newline at end of file diff --git a/ansible/roles/installation/tasks/main.yml b/ansible/roles/installation/tasks/main.yml index 6df9b2b9f..abda7ec68 100644 --- a/ansible/roles/installation/tasks/main.yml +++ b/ansible/roles/installation/tasks/main.yml @@ -53,9 +53,9 @@ TWILIO_AUTH_TOKEN: "{{ rails.twilio.auth_token }}" TWILIO_API_KEY_SID: "{{ rails.twilio.api_key.sid }}" TWILIO_API_KEY_SECRET: "{{ rails.twilio.api_key.secret }}" - 360_DIALOG_PARTNER_ID: "{{ rails.three_sixty_dialog.partner.id }}" - 360_DIALOG_PARTNER_USERNAME: "{{ rails.three_sixty_dialog.partner.username }}" - 360_DIALOG_PARTNER_PASSWORD: "{{ rails.three_sixty_dialog.partner.password }}" + THREE_SIXTY_DIALOG_PARTNER_ID: "{{ rails.three_sixty_dialog.partner.id }}" + THREE_SIXTY_DIALOG_PARTNER_USERNAME: "{{ rails.three_sixty_dialog.partner.username }}" + THREE_SIXTY_DIALOG_PARTNER_PASSWORD: "{{ rails.three_sixty_dialog.partner.password }}" community.general.docker_compose: project_src: /home/ansible diff --git a/app/models/setting.rb b/app/models/setting.rb index 214c918dc..43310a363 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -73,16 +73,17 @@ def self.onboarding_hero=(blob) field :whats_app_server_phone_number, readonly: true, default: ENV.fetch('WHATS_APP_SERVER_PHONE_NUMBER', nil) field :three_sixty_dialog_partner_token - field :three_sixty_dialog_partner_id, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_ID', nil) - field :three_sixty_dialog_partner_username, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_USERNAME', nil) - field :three_sixty_dialog_partner_password, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_PASSWORD', nil) - field :three_sixty_dialog_partner_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_PARTNER_REST_API_ENDPOINT', 'https://stoplight.io/mocks/360dialog/360dialog-partner-api/24588693') + field :three_sixty_dialog_partner_id, readonly: true, default: ENV.fetch('THREE_SIXTY_DIALOG_PARTNER_ID', nil) + field :three_sixty_dialog_partner_username, readonly: true, default: ENV.fetch('THREE_SIXTY_DIALOG_PARTNER_USERNAME', nil) + field :three_sixty_dialog_partner_password, readonly: true, default: ENV.fetch('THREE_SIXTY_DIALOG_PARTNER_PASSWORD', nil) + field :three_sixty_dialog_partner_rest_api_endpoint, readonly: true, default: ENV.fetch('THREE_SIXTY_DIALOG_PARTNER_REST_API_ENDPOINT', 'https://stoplight.io/mocks/360dialog/360dialog-partner-api/24588693') field :three_sixty_dialog_client_api_key field :three_sixty_dialog_client_id field :three_sixty_dialog_client_waba_account_id - field :three_sixty_dialog_whats_app_rest_api_endpoint, readonly: true, default: ENV.fetch('360_DIALOG_WHATS_APP_REST_API_ENDPOINT', 'https://waba-sandbox.360dialog.io') + field :three_sixty_dialog_whats_app_rest_api_endpoint, readonly: true, + default: ENV.fetch('THREE_SIXTY_DIALOG_WHATS_APP_REST_API_ENDPOINT', 'https://waba-sandbox.360dialog.io') field :three_sixty_dialog_whats_app_template_namespace field :inbound_email_password, readonly: true, default: ENV.fetch('RAILS_INBOUND_EMAIL_PASSWORD', nil) diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 8564f98d2..b0e94f84d 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -4,7 +4,7 @@ <%= t "dashboard.heading#{time_based_heading}", first_name: current_user.first_name %> <% end %> <%= c 'button', - link: "https://hub.360dialog.com/dashboard/app/#{ENV.fetch('360_DIALOG_PARTNER_ID', '')}/permissions?redirect_url=#{CGI.escape(whats_app_onboarding_successful_url)}", + link: "https://hub.360dialog.com/dashboard/app/#{ENV.fetch('THREE_SIXTY_DIALOG_PARTNER_ID', '')}/permissions?redirect_url=#{CGI.escape(whats_app_onboarding_successful_url)}", label: 'Connect to 360dialog', target: "_blank" %> diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 56c9b5c98..1af465f4a 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -31,9 +31,9 @@ x-prod-defaults: &x-prod-defaults TWILIO_AUTH_TOKEN: "${TWILIO_AUTH_TOKEN}" TWILIO_API_KEY_SID: "${TWILIO_API_KEY_SID}" TWILIO_API_KEY_SECRET: "${TWILIO_API_KEY_SECRET}" - 360_DIALOG_PARTNER_ID: "${360_DIALOG_PARTNER_ID}" - 360_DIALOG_PARTNER_USERNAME: "${360_DIALOG_PARTNER_USERNAME}" - 360_DIALOG_PARTNER_PASSWORD: "${360_DIALOG_PARTNER_PASSWORD}" + THREE_SIXTY_DIALOG_PARTNER_ID: "${THREE_SIXTY_DIALOG_PARTNER_ID}" + THREE_SIXTY_DIALOG_PARTNER_USERNAME: "${THREE_SIXTY_DIALOG_PARTNER_USERNAME}" + THREE_SIXTY_DIALOG_PARTNER_PASSWORD: "${THREE_SIXTY_DIALOG_PARTNER_PASSWORD}" volumes: - ./log:/app/log diff --git a/docker-compose.yml b/docker-compose.yml index 1916ef568..baa376d67 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,8 +8,8 @@ x-defaults: &x-defaults POSTGRES_PORT: "${POSTGRES_PORT:-5432}" SIGNAL_CLI_REST_API_ENDPOINT: "http://signal:8080" SIGNAL_CLI_REST_API_ATTACHMENT_PATH: "signal-cli-config/attachments/" - 360_DIALOG_PARTNER_REST_API_ENDPOINT: "https://hub.360dialog.io/api/v2" - 360_DIALOG_WHATS_APP_REST_API_ENDPOINT: "https://waba.360dialog.io/v1" + THREE_SIXTY_DIALOG_PARTNER_REST_API_ENDPOINT: "https://hub.360dialog.io/api/v2" + THREE_SIXTY_DIALOG_WHATS_APP_REST_API_ENDPOINT: "https://waba.360dialog.io/v1" services: app: From 215f87a5af568434e759110bef637e929fa29ab2 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 8 Aug 2023 20:37:48 +0200 Subject: [PATCH 26/57] Improve jobs - initialize setting, then save - use correct response code in handle_response --- .../outbound/three_sixty_dialog_text.rb | 2 +- app/jobs/whats_app_adapter/create_api_key.rb | 11 ++++++++--- app/jobs/whats_app_adapter/set_webhook_url.rb | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb index d0e4c5b4e..5ea2ce1b3 100644 --- a/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb +++ b/app/adapters/whats_app_adapter/outbound/three_sixty_dialog_text.rb @@ -21,7 +21,7 @@ def perform(payload:) def handle_response(response) case response.code.to_i - when 200 + when 201 Rails.logger.debug 'Great!' when 400..599 exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) diff --git a/app/jobs/whats_app_adapter/create_api_key.rb b/app/jobs/whats_app_adapter/create_api_key.rb index 4d6feb814..117f8e9e6 100644 --- a/app/jobs/whats_app_adapter/create_api_key.rb +++ b/app/jobs/whats_app_adapter/create_api_key.rb @@ -45,14 +45,19 @@ def fetch_token http.request(request) end token = JSON.parse(response.body)['access_token'] - Setting.three_sixty_dialog_partner_token = token + setting = Setting.find_or_initialize_by(var: :three_sixty_dialog_partner_token) + setting.value = token + setting.save end def handle_response(response) case response.code.to_i - when 200 + when 201 api_key = JSON.parse(response.body)['api_key'] - Setting.three_sixty_dialog_client_api_key = api_key + setting = Setting.find_or_initialize_by(var: :three_sixty_dialog_client_api_key) + setting.value = api_key + setting.save + WhatsAppAdapter::SetWebhookUrl.perform_later when 400..599 exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) ErrorNotifier.report(exception) diff --git a/app/jobs/whats_app_adapter/set_webhook_url.rb b/app/jobs/whats_app_adapter/set_webhook_url.rb index bd0cd58ba..f9b631cff 100644 --- a/app/jobs/whats_app_adapter/set_webhook_url.rb +++ b/app/jobs/whats_app_adapter/set_webhook_url.rb @@ -5,7 +5,7 @@ module WhatsAppAdapter class SetWebhookUrl < ApplicationJob def perform - return unless Setting.three_sixty_dialog_client_api_key + return unless Setting.three_sixty_dialog_client_api_key.present? base_uri = Setting.three_sixty_dialog_whats_app_rest_api_endpoint url = URI.parse("#{base_uri}/configs/webhook") @@ -23,7 +23,7 @@ def perform def handle_response(response) case response.code.to_i - when 200 + when 201 Rails.logger.debug 'Great!' when 400..599 exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: response.code, message: response.body) From fad7f428f01ff104ea9d531abce7678a8d7886f2 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 8 Aug 2023 20:39:15 +0200 Subject: [PATCH 27/57] Lint --- app/jobs/whats_app_adapter/set_webhook_url.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/whats_app_adapter/set_webhook_url.rb b/app/jobs/whats_app_adapter/set_webhook_url.rb index f9b631cff..3e72db58c 100644 --- a/app/jobs/whats_app_adapter/set_webhook_url.rb +++ b/app/jobs/whats_app_adapter/set_webhook_url.rb @@ -5,7 +5,7 @@ module WhatsAppAdapter class SetWebhookUrl < ApplicationJob def perform - return unless Setting.three_sixty_dialog_client_api_key.present? + return if Setting.three_sixty_dialog_client_api_key.blank? base_uri = Setting.three_sixty_dialog_whats_app_rest_api_endpoint url = URI.parse("#{base_uri}/configs/webhook") From 42f5ecb0d4d5b3e5eba530e379e1b5365452999f Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 9 Aug 2023 20:27:44 +0200 Subject: [PATCH 28/57] Move connect button to profile, use JS/modal --- .../profile_contributors_section.html.erb | 8 ++++--- .../whats_app_setup/whats_app_setup.css | 20 ++++++++++++++++++ .../whats_app_setup/whats_app_setup.html.erb | 20 ++++++++++++++++++ .../whats_app_setup/whats_app_setup.js | 21 +++++++++++++++++++ .../whats_app_setup/whats_app_setup.rb | 11 ++++++++++ app/views/dashboard/index.html.erb | 7 +------ app/views/profile/index.html.erb | 1 + config/locales/de.yml | 5 ++++- 8 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 app/components/whats_app_setup/whats_app_setup.css create mode 100644 app/components/whats_app_setup/whats_app_setup.html.erb create mode 100644 app/components/whats_app_setup/whats_app_setup.js create mode 100644 app/components/whats_app_setup/whats_app_setup.rb diff --git a/app/components/profile_contributors_section/profile_contributors_section.html.erb b/app/components/profile_contributors_section/profile_contributors_section.html.erb index 23c64ef83..e432a9c98 100644 --- a/app/components/profile_contributors_section/profile_contributors_section.html.erb +++ b/app/components/profile_contributors_section/profile_contributors_section.html.erb @@ -2,10 +2,12 @@ <%= c 'heading', style: :beta do %> <%= t('profile.contributors_section.main_heading') %> <% end %> -

<%= t('profile.contributors_section.number_of_contributors_of_business_plan_used', - contributors_count: organization.contributors.active.count, - allocated_contributors: organization.business_plan.number_of_contributors) +

+ <%= t('profile.contributors_section.number_of_contributors_of_business_plan_used', + contributors_count: organization.contributors.active.count, + allocated_contributors: organization.business_plan.number_of_contributors) %> +

<%= c 'flex' do %> <%= c 'contributors_status_bar', organization: organization %> <%= c 'copy_button', diff --git a/app/components/whats_app_setup/whats_app_setup.css b/app/components/whats_app_setup/whats_app_setup.css new file mode 100644 index 000000000..9bd647e90 --- /dev/null +++ b/app/components/whats_app_setup/whats_app_setup.css @@ -0,0 +1,20 @@ +.WhatsAppSetup { + display: flex; + align-items: center; +} + +.WhatsAppSetup-header { + flex-basis: 50%; +} + +.WhatsAppSetup-openModalButton > svg { + margin-right: var(--spacing-unit-xs); + width: var(--spacing-unit); + color: var(--color-primary); +} + +.WhatsAppSetup-openModalButton { + display: flex; + align-items: center; + margin-left: var(--spacing-unit); +} diff --git a/app/components/whats_app_setup/whats_app_setup.html.erb b/app/components/whats_app_setup/whats_app_setup.html.erb new file mode 100644 index 000000000..12bd11f7b --- /dev/null +++ b/app/components/whats_app_setup/whats_app_setup.html.erb @@ -0,0 +1,20 @@ +<%= c 'section', + styles: [:wide, :xlargeMarginTop, :noSpaceBetween], + **attrs, + data: { controller: 'whats-app-setup', + whats_app_setup_permissions_url_value: permissions_url + } do %> +
+ <%= c 'heading', style: :beta do %> + <%= t('.heading') %> + <% end %> +

<%= t('.setup_explained') %>

+
+ + <%= c 'button', + data: { action: 'whats-app-setup#openModal' }, + class: 'Button--secondary WhatsAppSetup-openModalButton' do %> + <%= svg 'plus-sign' %> + <%= t('.open_modal_button') %> + <% end %> +<% end %> diff --git a/app/components/whats_app_setup/whats_app_setup.js b/app/components/whats_app_setup/whats_app_setup.js new file mode 100644 index 000000000..eb63875c1 --- /dev/null +++ b/app/components/whats_app_setup/whats_app_setup.js @@ -0,0 +1,21 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static values = { + permissionsUrl: String, + }; + + connect() { + console.log('connected'); + } + + openModal() { + const windowFeatures = + 'toolbar=no, menubar=no, width=600, height=900, top=100, left=100'; + open( + this.permissionsUrlValue, + 'integratedOnboardingWindow', + windowFeatures + ); + } +} diff --git a/app/components/whats_app_setup/whats_app_setup.rb b/app/components/whats_app_setup/whats_app_setup.rb new file mode 100644 index 000000000..69d70ecfe --- /dev/null +++ b/app/components/whats_app_setup/whats_app_setup.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module WhatsAppSetup + class WhatsAppSetup < ApplicationComponent + private + + def permissions_url + "https://hub.360dialog.com/dashboard/app/#{ENV.fetch('THREE_SIXTY_DIALOG_PARTNER_ID', '')}/permissions?redirect_url=#{CGI.escape(whats_app_onboarding_successful_url)}" + end + end +end diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index b0e94f84d..99140afee 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -3,12 +3,7 @@ <%= c 'heading' do %> <%= t "dashboard.heading#{time_based_heading}", first_name: current_user.first_name %> <% end %> - <%= c 'button', - link: "https://hub.360dialog.com/dashboard/app/#{ENV.fetch('THREE_SIXTY_DIALOG_PARTNER_ID', '')}/permissions?redirect_url=#{CGI.escape(whats_app_onboarding_successful_url)}", - label: 'Connect to 360dialog', - target: "_blank" - %> - <%# <%= c 'new_request_link' %> + <%= c 'new_request_link' %> <% end %> diff --git a/app/views/profile/index.html.erb b/app/views/profile/index.html.erb index f4b19fa81..4f107089f 100644 --- a/app/views/profile/index.html.erb +++ b/app/views/profile/index.html.erb @@ -2,4 +2,5 @@ <%= c 'profile_header', organization: @organization, business_plans: @business_plans %> <%= c 'user_management', organization: @organization %> <%= c 'profile_contributors_section', organization: @organization %> + <%= c 'whats_app_setup' %> <% end %> diff --git a/config/locales/de.yml b/config/locales/de.yml index c1f15b373..da3f43d7d 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -353,7 +353,10 @@ de: empty_state: no_contributors: Du hast noch keine Mitglieder, verschick du Einladungslinks. filter_active: Keine Mitglieder mit diesen Filtern gefunden - + whats_app_setup: + heading: WhatsApp-Integration + setup_explained: Erteilen Sie unserem Business Solutions Provider, 360dialog, die Berechtigung, Ihren WhatsApp Business Manager zu verwalten, um WhatsApp als Kanal hinzuzufügen. + open_modal_button: WhatsApp einrichten mailer: unsubscribe: From 018410cd6686ab650ab9df61c68773228c8ab105 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 15 Aug 2023 15:36:04 +0200 Subject: [PATCH 29/57] Fix failing tests after refactor --- .../whats_app_adapter/twilio_outbound.rb | 2 +- ...tbound_spec.rb => twilio_outbound_spec.rb} | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) rename spec/adapters/whats_app_adapter/{outbound_spec.rb => twilio_outbound_spec.rb} (80%) diff --git a/app/adapters/whats_app_adapter/twilio_outbound.rb b/app/adapters/whats_app_adapter/twilio_outbound.rb index d83188663..a24705e29 100644 --- a/app/adapters/whats_app_adapter/twilio_outbound.rb +++ b/app/adapters/whats_app_adapter/twilio_outbound.rb @@ -87,7 +87,7 @@ def send_message(recipient, message) files = message.files if files.blank? - WhatsAppAdapter::Outbound::Text.perform_later + WhatsAppAdapter::Outbound::Text.perform_later(recipient: recipient, text: message.text) else files.each_with_index do |file, index| WhatsAppAdapter::Outbound::File.perform_later(recipient: recipient, text: index.zero? ? message.text : '', file: file) diff --git a/spec/adapters/whats_app_adapter/outbound_spec.rb b/spec/adapters/whats_app_adapter/twilio_outbound_spec.rb similarity index 80% rename from spec/adapters/whats_app_adapter/outbound_spec.rb rename to spec/adapters/whats_app_adapter/twilio_outbound_spec.rb index 1fcd181bc..48dc0c729 100644 --- a/spec/adapters/whats_app_adapter/outbound_spec.rb +++ b/spec/adapters/whats_app_adapter/twilio_outbound_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' require 'webmock/rspec' -RSpec.describe WhatsAppAdapter::Outbound do +RSpec.describe WhatsAppAdapter::TwilioOutbound do let(:adapter) { described_class.new } let(:message) { create(:message, text: 'WhatsApp as a channel is great, no?', broadcasted: true, recipient: contributor) } let(:contributor) { create(:contributor, email: nil) } @@ -15,7 +15,7 @@ subject { -> { described_class.send_welcome_message!(contributor) } } before { message } # we don't count the extra ::send here - it { should_not enqueue_job(described_class::Text) } + it { should_not enqueue_job(WhatsAppAdapter::Outbound::Text) } context 'contributor has a phone number' do let(:contributor) do @@ -26,7 +26,7 @@ ) end - it { should enqueue_job(described_class::Text).with(expected_job_args) } + it { should enqueue_job(WhatsAppAdapter::Outbound::Text).with(expected_job_args) } end end @@ -35,7 +35,7 @@ before { message } # we don't count the extra ::send here context '`whats_app_phone_number` blank' do - it { should_not enqueue_job(described_class::Text) } + it { should_not enqueue_job(WhatsAppAdapter::Outbound::Text) } end context 'given a WhatsApp contributor' do @@ -49,7 +49,7 @@ describe 'contributor has not sent a message within 24 hours' do it 'enqueues the Text job with WhatsApp template' do - expect { subject.call }.to(have_enqueued_job(described_class::Text).on_queue('default').with do |params| + expect { subject.call }.to(have_enqueued_job(WhatsAppAdapter::Outbound::Text).on_queue('default').with do |params| expect(params[:recipient]).to eq(contributor) expect(params[:text]).to include(contributor.first_name) expect(params[:text]).to include(message.request.title) @@ -61,7 +61,7 @@ before { contributor.update(whats_app_message_template_responded_at: Time.current) } it 'enqueues the Text job with the request text' do - expect { subject.call }.to(have_enqueued_job(described_class::Text).on_queue('default').with do |params| + expect { subject.call }.to(have_enqueued_job(WhatsAppAdapter::Outbound::Text).on_queue('default').with do |params| expect(params[:recipient]).to eq(contributor) expect(params[:text]).to eq(message.text) end) @@ -72,7 +72,7 @@ before { create(:message, sender: contributor) } it 'enqueues the Text job with the request text' do - expect { subject.call }.to(have_enqueued_job(described_class::Text).on_queue('default').with do |params| + expect { subject.call }.to(have_enqueued_job(WhatsAppAdapter::Outbound::Text).on_queue('default').with do |params| expect(params[:recipient]).to eq(contributor) expect(params[:text]).to eq(message.text) end) @@ -85,7 +85,7 @@ context 'contributor has not sent a message within 24 hours' do it 'enqueues the Text job with WhatsApp template' do - expect { subject.call }.to(have_enqueued_job(described_class::Text).on_queue('default').with do |params| + expect { subject.call }.to(have_enqueued_job(WhatsAppAdapter::Outbound::Text).on_queue('default').with do |params| expect(params[:recipient]).to eq(contributor) expect(params[:text]).to include(contributor.first_name) expect(params[:text]).to include(message.request.title) @@ -96,7 +96,7 @@ context 'contributor has sent a reply within 24 hours' do before { create(:message, sender: contributor) } it 'enqueues a File job with file, contributor, text' do - expect { subject.call }.to(have_enqueued_job(described_class::File).on_queue('default').with do |params| + expect { subject.call }.to(have_enqueued_job(WhatsAppAdapter::Outbound::File).on_queue('default').with do |params| expect(params[:file]).to eq(message.files.first) expect(params[:recipient]).to eq(contributor) expect(params[:text]).to eq(message.text) @@ -108,7 +108,7 @@ end describe '::freeform_message_permitted?(recipient)' do - subject { described_class.freeform_message_permitted?(contributor) } + subject { described_class.send(:freeform_message_permitted?, contributor) } describe 'template message' do context 'contributor has responded' do From 13bfe5b9ce1f931cadcbb726d60d630893ce8800 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 15 Aug 2023 15:43:18 +0200 Subject: [PATCH 30/57] Namespace WhatsAppAdapter callbacks to avoid duplicate const --- .../three_sixty_dialog_inbound.rb | 13 ++++++------- .../{inbound.rb => twilio_inbound.rb} | 15 +++++++-------- .../three_sixty_dialog_webhook_controller.rb | 12 ++++++------ app/controllers/whats_app/webhook_controller.rb | 14 +++++++------- 4 files changed, 26 insertions(+), 28 deletions(-) rename app/adapters/whats_app_adapter/{inbound.rb => twilio_inbound.rb} (93%) diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb index 721f34b9b..fb352a947 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb @@ -1,14 +1,13 @@ # frozen_string_literal: true module WhatsAppAdapter - UNKNOWN_CONTRIBUTOR = :unknown_contributor - UNSUPPORTED_CONTENT = :unsupported_content - REQUEST_FOR_MORE_INFO = :request_for_more_info - REQUEST_TO_RECEIVE_MESSAGE = :request_to_receive_message - UNSUBSCRIBE_CONTRIBUTOR = :unsubscribe_contributor - SUBSCRIBE_CONTRIBUTOR = :subscribe_contributor - class ThreeSixtyDialogInbound + UNKNOWN_CONTRIBUTOR = :unknown_contributor + UNSUPPORTED_CONTENT = :unsupported_content + REQUEST_FOR_MORE_INFO = :request_for_more_info + REQUEST_TO_RECEIVE_MESSAGE = :request_to_receive_message + UNSUBSCRIBE_CONTRIBUTOR = :unsubscribe_contributor + SUBSCRIBE_CONTRIBUTOR = :subscribe_contributor SUPPORTED_ATTACHMENT_TYPES = %w[image/jpg image/jpeg image/png image/gif audio/ogg video/mp4].freeze UNSUPPORTED_CONTENT_TYPES = %w[location contacts].freeze diff --git a/app/adapters/whats_app_adapter/inbound.rb b/app/adapters/whats_app_adapter/twilio_inbound.rb similarity index 93% rename from app/adapters/whats_app_adapter/inbound.rb rename to app/adapters/whats_app_adapter/twilio_inbound.rb index c733676bb..6fcbecd87 100644 --- a/app/adapters/whats_app_adapter/inbound.rb +++ b/app/adapters/whats_app_adapter/twilio_inbound.rb @@ -1,14 +1,13 @@ # frozen_string_literal: true module WhatsAppAdapter - UNKNOWN_CONTRIBUTOR = :unknown_contributor - UNSUPPORTED_CONTENT = :unsupported_content - REQUEST_FOR_MORE_INFO = :request_for_more_info - REQUEST_TO_RECEIVE_MESSAGE = :request_to_receive_message - UNSUBSCRIBE_CONTRIBUTOR = :unsubscribe_contributor - SUBSCRIBE_CONTRIBUTOR = :subscribe_contributor - - class Inbound + class TwilioInbound + UNKNOWN_CONTRIBUTOR = :unknown_contributor + UNSUPPORTED_CONTENT = :unsupported_content + REQUEST_FOR_MORE_INFO = :request_for_more_info + REQUEST_TO_RECEIVE_MESSAGE = :request_to_receive_message + UNSUBSCRIBE_CONTRIBUTOR = :unsubscribe_contributor + SUBSCRIBE_CONTRIBUTOR = :subscribe_contributor SUPPORTED_ATTACHMENT_TYPES = %w[image/jpg image/jpeg image/png image/gif audio/ogg video/mp4].freeze UNSUPPORTED_CONTENT_TYPES = %w[application text/vcard latitude longitude].freeze diff --git a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb index 6a333c1fe..875afda2f 100644 --- a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb +++ b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb @@ -15,27 +15,27 @@ def message adapter = WhatsAppAdapter::ThreeSixtyDialogInbound.new - adapter.on(WhatsAppAdapter::UNKNOWN_CONTRIBUTOR) do |whats_app_phone_number| + adapter.on(WhatsAppAdapter::ThreeSixtyDialogInbound::UNKNOWN_CONTRIBUTOR) do |whats_app_phone_number| handle_unknown_contributor(whats_app_phone_number) end - adapter.on(WhatsAppAdapter::REQUEST_FOR_MORE_INFO) do |contributor| + adapter.on(WhatsAppAdapter::ThreeSixtyDialogInbound::REQUEST_FOR_MORE_INFO) do |contributor| handle_request_for_more_info(contributor) end - adapter.on(WhatsAppAdapter::REQUEST_TO_RECEIVE_MESSAGE) do |contributor| + adapter.on(WhatsAppAdapter::ThreeSixtyDialogInbound::REQUEST_TO_RECEIVE_MESSAGE) do |contributor| handle_request_to_receive_message(contributor) end - adapter.on(WhatsAppAdapter::UNSUPPORTED_CONTENT) do |contributor| + adapter.on(WhatsAppAdapter::ThreeSixtyDialogInbound::UNSUPPORTED_CONTENT) do |contributor| WhatsAppAdapter::ThreeSixtyDialogOutbound.send_unsupported_content_message!(contributor) end - adapter.on(WhatsAppAdapter::UNSUBSCRIBE_CONTRIBUTOR) do |contributor| + adapter.on(WhatsAppAdapter::ThreeSixtyDialogInbound::UNSUBSCRIBE_CONTRIBUTOR) do |contributor| handle_unsubsribe_contributor(contributor) end - adapter.on(WhatsAppAdapter::SUBSCRIBE_CONTRIBUTOR) do |contributor| + adapter.on(WhatsAppAdapter::ThreeSixtyDialogInbound::SUBSCRIBE_CONTRIBUTOR) do |contributor| handle_subscribe_contributor(contributor) end diff --git a/app/controllers/whats_app/webhook_controller.rb b/app/controllers/whats_app/webhook_controller.rb index 93d038c77..b72f13690 100644 --- a/app/controllers/whats_app/webhook_controller.rb +++ b/app/controllers/whats_app/webhook_controller.rb @@ -8,29 +8,29 @@ class WebhookController < ApplicationController UNSUCCESSFUL_DELIVERY = %w[undelivered failed].freeze def message - adapter = WhatsAppAdapter::Inbound.new + adapter = WhatsAppAdapter::TwilioInbound.new - adapter.on(WhatsAppAdapter::UNKNOWN_CONTRIBUTOR) do |whats_app_phone_number| + adapter.on(WhatsAppAdapter::TwilioInbound::UNKNOWN_CONTRIBUTOR) do |whats_app_phone_number| handle_unknown_contributor(whats_app_phone_number) end - adapter.on(WhatsAppAdapter::REQUEST_FOR_MORE_INFO) do |contributor| + adapter.on(WhatsAppAdapter::TwilioInbound::REQUEST_FOR_MORE_INFO) do |contributor| handle_request_for_more_info(contributor) end - adapter.on(WhatsAppAdapter::REQUEST_TO_RECEIVE_MESSAGE) do |contributor, twilio_message_sid| + adapter.on(WhatsAppAdapter::TwilioInbound::REQUEST_TO_RECEIVE_MESSAGE) do |contributor, twilio_message_sid| handle_request_to_receive_message(contributor, twilio_message_sid) end - adapter.on(WhatsAppAdapter::UNSUPPORTED_CONTENT) do |contributor| + adapter.on(WhatsAppAdapter::TwilioInbound::UNSUPPORTED_CONTENT) do |contributor| WhatsAppAdapter::TwilioOutbound.send_unsupported_content_message!(contributor) end - adapter.on(WhatsAppAdapter::UNSUBSCRIBE_CONTRIBUTOR) do |contributor| + adapter.on(WhatsAppAdapter::TwilioInbound::UNSUBSCRIBE_CONTRIBUTOR) do |contributor| handle_unsubsribe_contributor(contributor) end - adapter.on(WhatsAppAdapter::SUBSCRIBE_CONTRIBUTOR) do |contributor| + adapter.on(WhatsAppAdapter::TwilioInbound::SUBSCRIBE_CONTRIBUTOR) do |contributor| handle_subscribe_contributor(contributor) end From 8a852d24899b6c0c1e5ed4ec901b1a6a2c1b6e13 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 15 Aug 2023 16:34:00 +0200 Subject: [PATCH 31/57] Add 360dialog check in contributor onboarding --- app/controllers/onboarding/whats_app_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/onboarding/whats_app_controller.rb b/app/controllers/onboarding/whats_app_controller.rb index 58dbcfb18..ea550ff00 100644 --- a/app/controllers/onboarding/whats_app_controller.rb +++ b/app/controllers/onboarding/whats_app_controller.rb @@ -11,7 +11,7 @@ def attr_name end def ensure_whats_app_is_set_up - return if Setting.whats_app_server_phone_number.present? + return if Setting.whats_app_server_phone_number.present? || Setting.three_sixty_dialog_client_api_key.present? raise ActionController::RoutingError, 'Not Found' end From 71ba923af8dbd4d6d2d9215764581ee7997f3430 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 15 Aug 2023 16:34:26 +0200 Subject: [PATCH 32/57] Fix welcome message template payload --- .../three_sixty_dialog_outbound.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb index 5f579045b..2ddfd7dfa 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb @@ -149,12 +149,23 @@ def welcome_message_payload(recipient) to: recipient.whats_app_phone_number.split('+').last, type: 'template', template: { - namespace: template_namespace, + namespace: Setting.three_sixty_dialog_whats_app_template_namespace, language: { policy: 'deterministic', code: 'de' }, - name: 'welcome_message' + name: 'welcome_message', + components: [ + { + type: 'body', + parameters: [ + { + type: 'text', + text: Setting.project_name + } + ] + } + ] } } end From 9be875e1348742452cfb6209c174f44f27bfb6fc Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 15 Aug 2023 18:52:59 +0200 Subject: [PATCH 33/57] Lint --- app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb index 2ddfd7dfa..1233f0726 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop:disable Metrics/ClassLength module WhatsAppAdapter class ThreeSixtyDialogOutbound class << self @@ -172,3 +173,4 @@ def welcome_message_payload(recipient) end end end +# rubocop:enable Metrics/ClassLength From 3d83c29cf79da39eb287fa672c61874e8d702b80 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Tue, 15 Aug 2023 20:32:43 +0200 Subject: [PATCH 34/57] Add requests specs, fix issues - error reporting was not working properly - fix handling of unsupported content --- .../three_sixty_dialog_inbound.rb | 15 +- .../whats_app_adapter/twilio_inbound.rb | 1 - .../three_sixty_dialog_webhook_controller.rb | 8 +- .../three_sixty_dialog_webhook_spec.rb | 262 ++++++++++++++++++ 4 files changed, 279 insertions(+), 7 deletions(-) create mode 100644 spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb index fb352a947..e2a7b91a4 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb @@ -8,8 +8,7 @@ class ThreeSixtyDialogInbound REQUEST_TO_RECEIVE_MESSAGE = :request_to_receive_message UNSUBSCRIBE_CONTRIBUTOR = :unsubscribe_contributor SUBSCRIBE_CONTRIBUTOR = :subscribe_contributor - SUPPORTED_ATTACHMENT_TYPES = %w[image/jpg image/jpeg image/png image/gif audio/ogg video/mp4].freeze - UNSUPPORTED_CONTENT_TYPES = %w[location contacts].freeze + UNSUPPORTED_CONTENT_TYPES = %w[location contacts document].freeze attr_reader :sender, :text, :message @@ -87,11 +86,11 @@ def initialize_unsupported_content(whats_app_message) def initialize_file(whats_app_message) message = whats_app_message[:messages].first - return [] unless message[:image] || message[:voice] || message[:video] + return [] unless file_type_supported?(message) file = Message::File.new - message_file = message[:image] || message[:voice] || message[:video] + message_file = supported_file(message) content_type = message_file[:mime_type] file_id = message_file[:id] @@ -105,6 +104,14 @@ def initialize_file(whats_app_message) [file] end + def file_type_supported?(message) + supported_file(message).present? + end + + def supported_file(message) + message[:image] || message[:voice] || message[:video] || message[:audio] + end + def unsupported_content?(whats_app_message) message = whats_app_message[:messages].first return unless message diff --git a/app/adapters/whats_app_adapter/twilio_inbound.rb b/app/adapters/whats_app_adapter/twilio_inbound.rb index 6fcbecd87..a5c9d0b05 100644 --- a/app/adapters/whats_app_adapter/twilio_inbound.rb +++ b/app/adapters/whats_app_adapter/twilio_inbound.rb @@ -8,7 +8,6 @@ class TwilioInbound REQUEST_TO_RECEIVE_MESSAGE = :request_to_receive_message UNSUBSCRIBE_CONTRIBUTOR = :unsubscribe_contributor SUBSCRIBE_CONTRIBUTOR = :subscribe_contributor - SUPPORTED_ATTACHMENT_TYPES = %w[image/jpg image/jpeg image/png image/gif audio/ogg video/mp4].freeze UNSUPPORTED_CONTENT_TYPES = %w[application text/vcard latitude longitude].freeze attr_reader :sender, :text, :message diff --git a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb index 875afda2f..42f91546d 100644 --- a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb +++ b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb @@ -11,7 +11,7 @@ def message head :ok return if params['statuses'].present? # TODO: Do we want to handle statuses? - handle_error(params['errors']) if params['errors'].present? + handle_error(params['messages'].first['errors'].first) if params['messages'].first['errors'].present? adapter = WhatsAppAdapter::ThreeSixtyDialogInbound.new @@ -61,7 +61,9 @@ def message_params { button: [:text] }, { image: %i[id mime_type sha256] }, { voice: %i[id mime_type sha256] }, { video: %i[id mime_type sha256] }, + { audio: %i[id mime_type sha256] }, { errors: %i[code details title] }, + { document: %i[filename id mime_type sha256] }, { location: %i[latitude longitude timestamp type] }, { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, { phones: %i[phone type wa_id] }, { urls: [] }, @@ -70,7 +72,9 @@ def message_params messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, { button: [:text] }, { image: %i[id mime_type sha256] }, { voice: %i[id mime_type sha256] }, { video: %i[id mime_type sha256] }, + { audio: %i[id mime_type sha256] }, { errors: %i[code details title] }, + { document: %i[filename id mime_type sha256] }, { location: %i[latitude longitude timestamp type] }, { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, { phones: %i[phone type wa_id] }, { urls: [] }, @@ -83,7 +87,7 @@ def create_api_key_params def handle_error(error) exception = WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: error['code'], message: error['title']) - ErrorNotifier.new(exception, context: { details: error['details'] }) + ErrorNotifier.report(exception, context: { details: error['details'] }) end def handle_request_to_receive_message(contributor) diff --git a/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb b/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb new file mode 100644 index 000000000..4b6c203f0 --- /dev/null +++ b/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb @@ -0,0 +1,262 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe WhatsApp::ThreeSixtyDialogWebhookController do + let(:whats_app_phone_number) { '+491511234567' } + let(:params) do + { contacts: [{ profile: { name: 'Matthew Rider' }, + wa_id: '491511234567' }], + messages: [{ from: '491511234567', + id: 'some_valid_id', + text: { body: 'Hey' }, + timestamp: '1692118778', + type: 'text' }], + three_sixty_dialog_webhook: { contacts: [{ profile: { name: 'Matthew Rider' }, + wa_id: '491511234567' }], + messages: [{ from: '491511234567', + id: 'some_valid_id', + text: { body: 'Hey' }, + timestamp: '1692118778', + type: 'text' }] } } + end + let(:text_payload) do + { + payload: { + recipient_type: 'individual', + to: contributor.whats_app_phone_number.split('+').last, + type: 'text', + text: { + body: text + } + } + } + end + let(:latest_message) { contributor.received_messages.first.text } + + subject { -> { post whats_app_three_sixty_dialog_webhook_path, params: params } } + + describe '#messages' do + before do + allow(Sentry).to receive(:capture_exception) + allow(Setting).to receive(:whats_app_server_phone_number).and_return('4915133311445') + allow(Request).to receive(:broadcast!).and_call_original + allow(Setting).to receive(:three_sixty_dialog_client_api_key).and_return('valid_api_key') + end + + describe 'statuses' do + let(:params) do + { + statuses: [{ id: 'some_valid_id', + message: { recipient_id: '491511234567' }, + status: 'read', + timestamp: '1691405467', + type: 'message' }], + three_sixty_dialog_webhook: { statuses: [{ id: 'some_valid_id', + message: { recipient_id: '491511234567' }, + status: 'read', timestamp: '1691405467', + type: 'message' }] } + } + end + + it 'ignores statuses' do + expect(WhatsAppAdapter::ThreeSixtyDialogInbound).not_to receive(:new) + + subject.call + end + end + + describe 'errors' do + let(:exception) { WhatsAppAdapter::ThreeSixtyDialogError.new(error_code: '501', message: 'Unsupported message type') } + before do + params[:messages] = [ + { errors: [{ + code: 501, + details: 'Message type is not currently supported', + title: 'Unsupported message type' + }], + from: '491511234567', + id: 'some_valid_id', + timestamp: '1691066820', + type: 'unknown' } + ] + allow(ErrorNotifier).to receive(:report) + end + + it 'reports the error' do + expect(ErrorNotifier).to receive(:report).with(exception, context: { details: 'Message type is not currently supported' }) + + subject.call + end + end + + describe 'unknown contributor' do + it 'does not create a message' do + expect { subject.call }.not_to change(Message, :count) + end + + it 'raises an error' do + expect(Sentry).to receive(:capture_exception).with( + WhatsAppAdapter::UnknownContributorError.new(whats_app_phone_number: '+491511234567') + ) + + subject.call + end + end + + describe 'given a contributor' do + let!(:contributor) { create(:contributor, whats_app_phone_number: whats_app_phone_number) } + let(:request) { create(:request) } + + before do + create(:message, request: request, recipient: contributor) + end + + context 'no message template sent' do + it 'creates a messsage' do + expect { subject.call }.to change(Message, :count).from(2).to(3) + end + end + + context 'responding to template' do + before { contributor.update(whats_app_message_template_sent_at: Time.current) } + let(:text) { latest_message } + + context 'request to receive latest message' do + it 'enqueues a job to send the latest received message' do + expect do + subject.call + end.to have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).on_queue('default').with(text_payload) + end + + it 'marks that contributor has responded to template message' do + expect { subject.call }.to change { + contributor.reload.whats_app_message_template_responded_at + }.from(nil).to(kind_of(ActiveSupport::TimeWithZone)) + end + end + end + + context 'request for more info' do + before { params[:messages].first[:text][:body] = 'Mehr Infos' } + let(:text) { [Setting.about, "_#{I18n.t('adapter.whats_app.unsubscribe.instructions')}_"].join("\n\n") } + + it 'marks that contributor has responded to template message' do + expect { subject.call }.to change { + contributor.reload.whats_app_message_template_responded_at + }.from(nil).to(kind_of(ActiveSupport::TimeWithZone)) + end + + it 'enqueues a job to send more info message' do + expect do + subject.call + end.to have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).on_queue('default').with(text_payload) + end + + context 'does not enqueue a job' do + let(:text) { latest_message } + + it 'to send the latest received message' do + expect { subject.call }.not_to have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).with(text_payload) + end + end + end + + context 'request to unsubscribe' do + let!(:admin) { create_list(:user, 2, admin: true) } + let!(:non_admin_user) { create(:user) } + + before { params[:messages].first[:text][:body] = 'Abbestellen' } + let(:text) do + [I18n.t('adapter.whats_app.unsubscribe.successful'), "_#{I18n.t('adapter.whats_app.subscribe.instructions')}_"].join("\n\n") + end + + it 'marks contributor as inactive' do + expect { subject.call }.to change { contributor.reload.deactivated_at }.from(nil).to(kind_of(ActiveSupport::TimeWithZone)) + end + + it 'enqueues a job to inform the contributor of successful unsubscribe' do + expect do + subject.call + end.to have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).on_queue('default').with(text_payload) + end + + it_behaves_like 'an ActivityNotification', 'ContributorMarkedInactive' + + it 'enqueues a job to inform admin' do + expect { subject.call }.to have_enqueued_job.on_queue('default').with( + 'PostmarkAdapter::Outbound', + 'contributor_marked_as_inactive_email', + 'deliver_now', # How ActionMailer works in test environment, even though in production we call deliver_later + { + params: { admin: an_instance_of(User), contributor: contributor }, + args: [] + } + ).exactly(2).times + end + + context 'does not enqueue a job' do + let(:text) { latest_message } + + it 'to send the latest received message' do + expect { subject.call }.not_to have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).with(text_payload) + end + end + end + + context 'request to re-subscribe' do + let!(:admin) { create_list(:user, 2, admin: true) } + let!(:non_admin_user) { create(:user) } + + before do + contributor.update(deactivated_at: Time.current) + params[:messages].first[:text][:body] = 'Bestellen' + end + + let(:text) do + I18n.t('adapter.whats_app.welcome_message', project_name: Setting.project_name) + end + + it 'marks contributor as active' do + expect { subject.call }.to change { contributor.reload.deactivated_at }.from(kind_of(ActiveSupport::TimeWithZone)).to(nil) + end + + it 'marks that contributor has responded to template message' do + expect { subject.call }.to change { + contributor.reload.whats_app_message_template_responded_at + }.from(nil).to(kind_of(ActiveSupport::TimeWithZone)) + end + + it 'enqueues a job to welcome contributor' do + expect do + subject.call + end.to have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).on_queue('default').with(text_payload) + end + + it_behaves_like 'an ActivityNotification', 'ContributorSubscribed' + + it 'enqueues a job to inform admin' do + expect { subject.call }.to have_enqueued_job.on_queue('default').with( + 'PostmarkAdapter::Outbound', + 'contributor_subscribed_email', + 'deliver_now', # How ActionMailer works in test environment, even though in production we call deliver_later + { + params: { admin: an_instance_of(User), contributor: contributor }, + args: [] + } + ).exactly(2).times + end + + context 'does not enqueue a job' do + let(:text) { latest_message } + + it 'to send the latest received message' do + expect { subject.call }.not_to have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).with(text_payload) + end + end + + # TODO: Write test cases for unsupported content + end + end + end +end From 3ca3b89fb7069480a4828cf446f817134aa28974 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 16 Aug 2023 19:49:21 +0200 Subject: [PATCH 35/57] Add tests for files --- .../three_sixty_dialog_webhook_controller.rb | 4 +- spec/factories/organizations.rb | 1 + .../three_sixty_dialog_webhook_spec.rb | 208 +++++++++++++++++- 3 files changed, 210 insertions(+), 3 deletions(-) diff --git a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb index 42f91546d..797169e31 100644 --- a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb +++ b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb @@ -64,7 +64,7 @@ def message_params { audio: %i[id mime_type sha256] }, { errors: %i[code details title] }, { document: %i[filename id mime_type sha256] }, - { location: %i[latitude longitude timestamp type] }, + { location: %i[latitude longitude] }, { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, { phones: %i[phone type wa_id] }, { urls: [] }, { name: %i[first_name formatted_name last_name] }] }]] }, @@ -75,7 +75,7 @@ def message_params { audio: %i[id mime_type sha256] }, { errors: %i[code details title] }, { document: %i[filename id mime_type sha256] }, - { location: %i[latitude longitude timestamp type] }, + { location: %i[latitude longitude] }, { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, { phones: %i[phone type wa_id] }, { urls: [] }, { name: %i[first_name formatted_name last_name] }] }]) diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb index c089f1ff7..4396da69a 100644 --- a/spec/factories/organizations.rb +++ b/spec/factories/organizations.rb @@ -4,6 +4,7 @@ factory :organization do name { '100eyes' } upgrade_discount { 10 } + contact_person { create(:user) } transient do users_count { 0 } diff --git a/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb b/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb index 4b6c203f0..821af2584 100644 --- a/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb +++ b/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'rails_helper' +require 'webmock/rspec' RSpec.describe WhatsApp::ThreeSixtyDialogWebhookController do let(:whats_app_phone_number) { '+491511234567' } @@ -255,7 +256,212 @@ end end - # TODO: Write test cases for unsupported content + context 'files' do + let(:message) { params[:messages].first } + let(:fetch_file_url) { "#{Setting.three_sixty_dialog_whats_app_rest_api_endpoint}/media/some_valid_id" } + + before { message[:text] = nil } + + context 'supported content' do + before { stub_request(:get, fetch_file_url).to_return(status: 200, body: 'downloaded_file') } + + context 'voice' do + let(:voice) do + { + id: 'some_valid_id', + mime_type: 'audio/ogg; codecs=opus', + sha256: 'sha256_hash' + } + end + before do + message[:type] = 'voice' + message[:voice] = voice + end + + it 'creates a new Message::File' do + expect { subject.call }.to change(Message::File, :count).from(0).to(1) + end + + it 'creates a new Message' do + expect { subject.call }.to change(Message, :count).from(2).to(3) + end + + it 'attaches the file to the message' do + subject.call + + expect(Message.first.files.first).to eq(Message::File.first) + end + end + + context 'voice' do + let(:voice) do + { + id: 'some_valid_id', + mime_type: 'audio/ogg; codecs=opus', + sha256: 'sha256_hash' + } + end + before do + message[:type] = 'voice' + message[:voice] = voice + end + + it 'creates a new Message::File' do + expect { subject.call }.to change(Message::File, :count).from(0).to(1) + end + + it 'creates a new Message' do + expect { subject.call }.to change(Message, :count).from(2).to(3) + end + + it 'attaches the file to the message with its mime_type' do + subject.call + + latest_message = Message.where(sender: contributor).first + expect(latest_message.files.first).to eq(Message::File.first) + expect(latest_message.files.first.attachment.content_type).to eq(message[:voice][:mime_type]) + end + end + + context 'video' do + let(:video) do + { + id: 'some_valid_id', + mime_type: 'video/mp4', + sha256: 'sha256_hash' + } + end + before do + message[:type] = 'video' + message[:video] = video + end + + it 'creates a new Message::File' do + expect { subject.call }.to change(Message::File, :count).from(0).to(1) + end + + it 'creates a new Message' do + expect { subject.call }.to change(Message, :count).from(2).to(3) + end + + it 'attaches the file to the message with its mime_type' do + subject.call + + latest_message = Message.where(sender: contributor).first + expect(latest_message.files.first).to eq(Message::File.first) + expect(latest_message.files.first.attachment.content_type).to eq(message[:video][:mime_type]) + end + end + + context 'audio' do + let(:audio) do + { + id: 'some_valid_id', + mime_type: 'audio/ogg', + sha256: 'sha256_hash' + } + end + before do + message[:type] = 'audio' + message[:audio] = audio + end + + it 'creates a new Message::File' do + expect { subject.call }.to change(Message::File, :count).from(0).to(1) + end + + it 'creates a new Message' do + expect { subject.call }.to change(Message, :count).from(2).to(3) + end + + it 'attaches the file to the message with its mime_type' do + subject.call + + latest_message = Message.where(sender: contributor).first + expect(latest_message.files.first).to eq(Message::File.first) + expect(latest_message.files.first.attachment.content_type).to eq(message[:audio][:mime_type]) + end + end + end + + context 'unsupported content' do + let(:text) do + I18n.t('adapter.whats_app.unsupported_content_template', first_name: contributor.first_name, + contact_person: contributor.organization.contact_person.name) + end + + context 'document' do + let(:document) do + { + filename: 'animated-cat-image-0056.gif', + id: 'some_valid_id', + mime_type: 'image/gif', + sha256: 'sha256_hash' + } + end + before do + message[:type] = 'document' + message[:document] = document + end + + it 'sends a message to contributor to let them know the message type is not supported' do + expect { subject.call }.to have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).with(text_payload) + end + end + + context 'location' do + let(:location) do + { + latitude: '22.9871', + longitude: '43.2048' + } + end + before do + message[:type] = 'location' + message[:location] = location + end + + it 'sends a message to contributor to let them know the message type is not supported' do + expect { subject.call }.to have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).with(text_payload) + end + end + + context 'contacts' do + let(:contacts) do + { + contacts: [ + { addresses: [], + emails: [], + ims: [], + name: { + first_name: '360dialog', + formatted_name: '360dialog Sandbox', + last_name: 'Sandbox' + }, + org: {}, + phones: [ + { phone: '+49 30 609859535', + type: 'Mobile', + wa_id: '4930609859535' } + ], urls: [] } + ], + from: '4915143416265', + id: 'some_valid_id', + timestamp: '1692123428', + type: 'contacts' + } + end + before do + message[:type] = 'contacts' + message[:contacts] = contacts + end + + it 'sends a message to contributor to let them know the message type is not supported' do + expect { subject.call }.to have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).with(text_payload) + end + end + end + end end end end From 5ea5e820a69933bc5089cb9dc67ae3ceeb9d0ca4 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 23 Aug 2023 17:28:03 +0200 Subject: [PATCH 36/57] Add WhatsApp button when configured with 360dialog --- app/controllers/onboarding_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/onboarding_controller.rb b/app/controllers/onboarding_controller.rb index 5ddd7f855..a2a4f74ba 100644 --- a/app/controllers/onboarding_controller.rb +++ b/app/controllers/onboarding_controller.rb @@ -15,7 +15,7 @@ def index @jwt = jwt_param @channels = %w[threema telegram email] @channels << 'signal' if Setting.signal_server_phone_number.present? - @channels << 'whats_app' if Setting.whats_app_server_phone_number.present? + @channels << 'whats_app' if Setting.whats_app_server_phone_number.present? || Setting.three_sixty_dialog_client_api_key.present? end def success; end From 24c4572ec2e3a2ba8edfb266cb1be16eee027f92 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 23 Aug 2023 18:57:28 +0200 Subject: [PATCH 37/57] Allow CreateApiKey job to run even with api key - We might want to create a new api key for any number of reasons. If the api key is exposed, for example. --- app/jobs/whats_app_adapter/create_api_key.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/jobs/whats_app_adapter/create_api_key.rb b/app/jobs/whats_app_adapter/create_api_key.rb index 117f8e9e6..1e4808f94 100644 --- a/app/jobs/whats_app_adapter/create_api_key.rb +++ b/app/jobs/whats_app_adapter/create_api_key.rb @@ -5,8 +5,6 @@ module WhatsAppAdapter class CreateApiKey < ApplicationJob def perform(channel_id:) - return if Setting.three_sixty_dialog_client_api_key.present? - @base_uri = Setting.three_sixty_dialog_partner_rest_api_endpoint token = Setting.find_by(var: 'three_sixty_dialog_partner_token') From 24c403d0a7ac7ccd565a219cd76b830af1c8532e Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 23 Aug 2023 19:00:43 +0200 Subject: [PATCH 38/57] Save captions, allow certain document types --- .../three_sixty_dialog_inbound.rb | 15 ++++++++++----- .../three_sixty_dialog_webhook_controller.rb | 8 ++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb index e2a7b91a4..c78d495b8 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb @@ -8,7 +8,7 @@ class ThreeSixtyDialogInbound REQUEST_TO_RECEIVE_MESSAGE = :request_to_receive_message UNSUBSCRIBE_CONTRIBUTOR = :unsubscribe_contributor SUBSCRIBE_CONTRIBUTOR = :subscribe_contributor - UNSUPPORTED_CONTENT_TYPES = %w[location contacts document].freeze + UNSUPPORTED_CONTENT_TYPES = %w[location contacts application].freeze attr_reader :sender, :text, :message @@ -61,7 +61,7 @@ def initialize_sender(whats_app_message) def initialize_message(whats_app_message) message = whats_app_message[:messages].first - text = message[:text]&.dig(:body) || message[:button]&.dig(:text) + text = message[:text]&.dig(:body) || message[:button]&.dig(:text) || supported_file(message)&.dig(:caption) trigger(REQUEST_FOR_MORE_INFO, sender) if request_for_more_info?(text) trigger(UNSUBSCRIBE_CONTRIBUTOR, sender) if unsubscribe_text?(text) @@ -93,10 +93,11 @@ def initialize_file(whats_app_message) message_file = supported_file(message) content_type = message_file[:mime_type] file_id = message_file[:id] + filename = message_file[:filename] || file_id file.attachment.attach( io: StringIO.new(fetch_file(file_id)), - filename: file_id, + filename: filename, content_type: content_type, identify: false ) @@ -105,11 +106,13 @@ def initialize_file(whats_app_message) end def file_type_supported?(message) - supported_file(message).present? + supported_file = message[:image] || message[:voice] || message[:video] || message[:audio] || + (message[:document] && UNSUPPORTED_CONTENT_TYPES.none? { |type| message[:document][:mime_type].include?(type) }) + supported_file.present? end def supported_file(message) - message[:image] || message[:voice] || message[:video] || message[:audio] + message[:image] || message[:voice] || message[:video] || message[:audio] || message[:document] end def unsupported_content?(whats_app_message) @@ -118,6 +121,8 @@ def unsupported_content?(whats_app_message) unsupported_content = message.keys.any? do |key| UNSUPPORTED_CONTENT_TYPES.include?(key) + end || UNSUPPORTED_CONTENT_TYPES.any? do |type| + message[:document]&.dig(:mime_type) && message[:document][:mime_type].include?(type) end errors = message[:errors] return unsupported_content unless errors diff --git a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb index 797169e31..3af62ebda 100644 --- a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb +++ b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb @@ -58,9 +58,9 @@ def create_api_key def message_params params.permit({ three_sixty_dialog_webhook: [contacts: [:wa_id, { profile: [:name] }], messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, - { button: [:text] }, { image: %i[id mime_type sha256] }, + { button: [:text] }, { image: %i[id mime_type sha256 caption] }, { voice: %i[id mime_type sha256] }, - { video: %i[id mime_type sha256] }, + { video: %i[id mime_type sha256 caption] }, { audio: %i[id mime_type sha256] }, { errors: %i[code details title] }, { document: %i[filename id mime_type sha256] }, @@ -70,8 +70,8 @@ def message_params { name: %i[first_name formatted_name last_name] }] }]] }, contacts: [:wa_id, { profile: [:name] }], messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, { button: [:text] }, - { image: %i[id mime_type sha256] }, { voice: %i[id mime_type sha256] }, - { video: %i[id mime_type sha256] }, + { image: %i[id mime_type sha256 caption] }, { voice: %i[id mime_type sha256] }, + { video: %i[id mime_type sha256 caption] }, { audio: %i[id mime_type sha256] }, { errors: %i[code details title] }, { document: %i[filename id mime_type sha256] }, From f5e04b79f83722f6f97fdbe3765a7bc85ce16e4c Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 23 Aug 2023 19:01:09 +0200 Subject: [PATCH 39/57] Add/refactor specs --- .../three_sixty_dialog_inbound_spec.rb | 488 ++++++++++++++++++ .../three_sixty_dialog_webhook_spec.rb | 102 +++- 2 files changed, 576 insertions(+), 14 deletions(-) create mode 100644 spec/adapters/whats_app_adapter/three_sixty_dialog_inbound_spec.rb diff --git a/spec/adapters/whats_app_adapter/three_sixty_dialog_inbound_spec.rb b/spec/adapters/whats_app_adapter/three_sixty_dialog_inbound_spec.rb new file mode 100644 index 000000000..55750d2e9 --- /dev/null +++ b/spec/adapters/whats_app_adapter/three_sixty_dialog_inbound_spec.rb @@ -0,0 +1,488 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'webmock/rspec' + +RSpec.describe WhatsAppAdapter::ThreeSixtyDialogInbound do + let(:adapter) { described_class.new } + let(:phone_number) { '+491511234567' } + let(:whats_app_message) do + { contacts: [{ profile: { name: 'Matthew Rider' }, + wa_id: '491511234567' }], + messages: [{ from: '491511234567', + id: 'some_valid_id', + text: { body: 'Hey' }, + timestamp: '1692118778', + type: 'text' }], + three_sixty_dialog_webhook: { contacts: [{ profile: { name: 'Matthew Rider' }, + wa_id: '491511234567' }], + messages: [{ from: '491511234567', + id: 'some_valid_id', + text: { body: 'Hey' }, + timestamp: '1692118778', + type: 'text' }] } } + end + let(:whats_app_message_with_attachment) do + { contacts: [{ profile: { name: 'Matthew Rider' }, + wa_id: '491511234567' }], + messages: [{ from: '491511234567', + id: 'some_valid_id', + image: { + caption: 'Look how cute', + id: 'some_valid_id', + mime_type: 'image/jpeg', + sha256: 'sha256_hash' + }, + timestamp: '1692118778', + type: 'image' }], + three_sixty_dialog_webhook: { contacts: [{ profile: { name: 'Matthew Rider' }, + wa_id: '491511234567' }], + messages: [{ from: '491511234567', + id: 'some_valid_id', + image: { + caption: 'Look how cute', + id: 'some_valid_id', + mime_type: 'image/jpeg', + sha256: 'sha256_hash' + }, + timestamp: '1692118778', + type: 'image' }] } } + end + + let!(:contributor) { create(:contributor, whats_app_phone_number: phone_number) } + let(:fetch_file_url) { "#{Setting.three_sixty_dialog_whats_app_rest_api_endpoint}/media/some_valid_id" } + + describe '#consume' do + let(:message) do + adapter.consume(whats_app_message) do |message| + message + end + end + + describe '|message| block argument' do + subject { message } + it { is_expected.to be_a(Message) } + + context 'from an unknown contributor' do + let!(:phone_number) { '+495555555' } + + it { is_expected.to be(nil) } + end + + context 'given a message with text and an attachment' do + let(:whats_app_message) { whats_app_message_with_attachment } + + before { stub_request(:get, fetch_file_url).to_return(status: 200, body: 'downloaded_file') } + + it 'is expected to store message text and attached file' do + expect(message.text).to eq('Look how cute') + expect(message.files.first.attachment).to be_attached + end + end + end + + describe '|message|text' do + subject { message.text } + + context 'given a whats_app_message with a `message`' do + it { is_expected.to eq('Hey') } + end + + context 'given a whats_app_message without a `message` and with an attachment' do + let(:whats_app_message) { whats_app_message_with_attachment } + before do + whats_app_message[:messages].first[:image][:caption] = nil + stub_request(:get, fetch_file_url).to_return(status: 200, body: 'downloaded_file') + end + + it { is_expected.to be(nil) } + end + end + + describe '|message|raw_data' do + subject { message.raw_data } + it { is_expected.to be_attached } + end + + describe '#sender' do + subject { message.sender } + + it { is_expected.to eq(contributor) } + end + + describe '|message|files' do + let(:whats_app_message) { whats_app_message_with_attachment } + + before do + stub_request(:get, fetch_file_url).to_return(status: 200, body: 'downloaded_file') + end + + describe 'handling different content types' do + let(:file) { message.files.first } + subject { file.attachment } + + context 'given an audio file' do + before do + first_message = whats_app_message[:messages].first + first_message[:type] = 'voice' + first_message.delete(:image) + first_message[:audio] = { + id: 'some_valid_id', + mime_type: 'audio/ogg', + sha256: 'sha256_hash' + } + end + + it { is_expected.to be_attached } + + it 'preserves the content_type' do + expect(subject.blob.content_type).to eq('audio/ogg') + end + end + + context 'given an audio/mpeg file' do + before do + first_message = whats_app_message[:messages].first + first_message[:type] = 'audio' + first_message.delete(:image) + first_message[:audio] = { + id: 'some_valid_id', + mime_type: 'audio/mpeg', + sha256: 'sha256_hash' + } + end + + it { is_expected.to be_attached } + + it 'preserves the content_type' do + expect(subject.blob.content_type).to eq('audio/mpeg') + end + end + + context 'given an image file' do + it { is_expected.to be_attached } + + it 'preserves the content_type' do + expect(subject.blob.content_type).to eq('image/jpeg') + end + end + + context 'given attachment without filename' do + it { is_expected.to be_attached } + + it 'sets a fallback filename based on external file id' do + expect(subject.filename.to_s).to eq('some_valid_id') + end + end + + context 'given a supported document' do + before do + first_message = whats_app_message[:messages].first + first_message[:type] = 'document' + first_message.delete(:image) + first_message[:document] = { + filename: 'AUD-12345.mpeg', + id: 'some_valid_id', + mime_type: 'audio/mpeg', + sha256: 'sha256_hash' + } + end + + context 'with a filename' do + it { is_expected.to be_attached } + + it 'favors the filename' do + expect(subject.filename.to_s).to eq('AUD-12345.mpeg') + end + end + end + + context 'given an unsupported document' do + subject { message.files } + + before do + first_message = whats_app_message[:messages].first + first_message[:type] = 'document' + first_message.delete(:image) + first_message[:document] = { + filename: 'Comprovante.pdf', + id: 'some_valid_id', + mime_type: 'application/pdf', + sha256: 'sha256_hash' + } + end + + it { is_expected.to be_empty } + end + end + end + end + + describe '#on' do + describe 'UNKNOWN_CONTRIBUTOR' do + let(:unknown_contributor_callback) { spy('unknown_contributor_callback') } + + before do + adapter.on(WhatsAppAdapter::ThreeSixtyDialogInbound::UNKNOWN_CONTRIBUTOR) do |whats_app_phone_number| + unknown_contributor_callback.call(whats_app_phone_number) + end + end + + subject do + adapter.consume(whats_app_message) + unknown_contributor_callback + end + + describe 'if the sender is a contributor ' do + it { should_not have_received(:call) } + end + + describe 'if the sender is unknown' do + before { whats_app_message[:contacts].first[:wa_id] = '4955443322' } + it { should have_received(:call).with('+4955443322') } + end + end + + describe 'UNSUPPORTED_CONTENT' do + let(:unsupported_content_callback) { spy('unsupported_content_callback') } + + before do + adapter.on(WhatsAppAdapter::ThreeSixtyDialogInbound::UNSUPPORTED_CONTENT) do |sender| + unsupported_content_callback.call(sender) + end + end + + subject do + adapter.consume(whats_app_message) + unsupported_content_callback + end + + describe 'supported content' do + context 'if the message is a plaintext message' do + it { should_not have_received(:call) } + end + + context 'files' do + let(:message) { whats_app_message[:messages].first } + + before do + message.delete(:text) + stub_request(:get, fetch_file_url).to_return(status: 200, body: 'downloaded_file') + end + + context 'image' do + let(:image) do + { + id: 'some_valid_id', + mime_type: 'image/jpeg', + sha256: 'sha256_hash' + } + end + + before do + message[:type] = 'image' + message[:image] = image + end + + it { should_not have_received(:call) } + end + + context 'voice' do + let(:voice) do + { + id: 'some_valid_id', + mime_type: 'audio/ogg; codecs=opus', + sha256: 'sha256_hash' + } + end + before do + message[:type] = 'voice' + message[:voice] = voice + end + + it { should_not have_received(:call) } + end + + context 'video' do + let(:video) do + { + id: 'some_valid_id', + mime_type: 'video/mp4', + sha256: 'sha256_hash' + } + end + before do + message[:type] = 'video' + message[:video] = video + end + + it { should_not have_received(:call) } + end + + context 'audio' do + let(:audio) do + { + id: 'some_valid_id', + mime_type: 'audio/ogg', + sha256: 'sha256_hash' + } + end + before do + message[:type] = 'audio' + message[:audio] = audio + end + + it { should_not have_received(:call) } + end + + context 'document' do + context 'image' do + let(:document) do + { + filename: 'animated-cat-image-0056.gif', + id: 'some_valid_id', + mime_type: 'image/gif', + sha256: 'sha256_hash' + } + end + + before do + message[:type] = 'document' + message[:document] = document + end + + it { should_not have_received(:call) } + end + + context 'audio' do + let(:document) do + { + filename: 'AUD-12345.opus', + id: 'some_valid_id', + mime_type: 'audio/ogg', + sha256: 'sha256_hash' + } + end + + before do + message[:type] = 'document' + message[:document] = document + end + + it { should_not have_received(:call) } + end + + context 'video' do + let(:document) do + { + filename: 'VID_12345.mp4', + id: 'some_valid_id', + mime_type: 'video/mp4', + sha256: 'sha256_hash' + } + end + + before do + message[:type] = 'document' + message[:document] = document + end + + it { should_not have_received(:call) } + end + end + end + end + + describe 'unsupported content' do + let(:message) { whats_app_message[:messages].first } + + before do + message.delete(:text) + stub_request(:get, fetch_file_url).to_return(status: 200, body: 'downloaded_file') + end + + context 'document|pdf|' do + let(:document) do + { + filename: 'Comprovante.pdf', + id: 'some_valid_id', + mime_type: 'application/pdf', + sha256: 'sha256_hash' + } + end + before do + message[:type] = 'document' + message[:document] = document + end + + it { should have_received(:call).with(contributor) } + end + + context 'document|docx|' do + let(:document) do + { + filename: 'price-list.docx', + id: 'some_valid_id', + mime_type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + sha256: 'sha256_hash' + } + end + before do + message[:type] = 'document' + message[:document] = document + end + + it { should have_received(:call).with(contributor) } + end + + context 'location' do + let(:location) do + { + latitude: '22.9871', + longitude: '43.2048' + } + end + before do + message[:type] = 'location' + message[:location] = location + end + + it { should have_received(:call).with(contributor) } + end + + context 'contacts' do + let(:contacts) do + { + contacts: [ + { addresses: [], + emails: [], + ims: [], + name: { + first_name: '360dialog', + formatted_name: '360dialog Sandbox', + last_name: 'Sandbox' + }, + org: {}, + phones: [ + { phone: '+49 30 609859535', + type: 'Mobile', + wa_id: '4930609859535' } + ], urls: [] } + ], + from: '4915143416265', + id: 'some_valid_id', + timestamp: '1692123428', + type: 'contacts' + } + end + before do + message[:type] = 'contacts' + message[:contacts] = contacts + end + + it { should have_received(:call).with(contributor) } + end + end + end + end +end diff --git a/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb b/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb index 821af2584..bded2eb8c 100644 --- a/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb +++ b/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb @@ -260,22 +260,22 @@ let(:message) { params[:messages].first } let(:fetch_file_url) { "#{Setting.three_sixty_dialog_whats_app_rest_api_endpoint}/media/some_valid_id" } - before { message[:text] = nil } + before { message.delete(:text) } context 'supported content' do before { stub_request(:get, fetch_file_url).to_return(status: 200, body: 'downloaded_file') } - context 'voice' do - let(:voice) do + context 'image' do + let(:image) do { id: 'some_valid_id', - mime_type: 'audio/ogg; codecs=opus', + mime_type: 'image/jpeg', sha256: 'sha256_hash' } end before do - message[:type] = 'voice' - message[:voice] = voice + message[:type] = 'image' + message[:image] = image end it 'creates a new Message::File' do @@ -286,10 +286,12 @@ expect { subject.call }.to change(Message, :count).from(2).to(3) end - it 'attaches the file to the message' do + it 'attaches the file to the message with its mime_type' do subject.call - expect(Message.first.files.first).to eq(Message::File.first) + latest_message = Message.where(sender: contributor).first + expect(latest_message.files.first).to eq(Message::File.first) + expect(latest_message.files.first.attachment.content_type).to eq(message[:image][:mime_type]) end end @@ -314,12 +316,10 @@ expect { subject.call }.to change(Message, :count).from(2).to(3) end - it 'attaches the file to the message with its mime_type' do + it 'attaches the file to the message' do subject.call - latest_message = Message.where(sender: contributor).first - expect(latest_message.files.first).to eq(Message::File.first) - expect(latest_message.files.first.attachment.content_type).to eq(message[:voice][:mime_type]) + expect(Message.first.files.first).to eq(Message::File.first) end end @@ -382,6 +382,80 @@ expect(latest_message.files.first.attachment.content_type).to eq(message[:audio][:mime_type]) end end + + context 'document' do + context 'image' do + let(:document) do + { + filename: 'animated-cat-image-0056.gif', + id: 'some_valid_id', + mime_type: 'image/gif', + sha256: 'sha256_hash' + } + end + + before do + message[:type] = 'document' + message[:document] = document + end + + it 'attaches the file to the message with its mime_type' do + subject.call + + latest_message = Message.where(sender: contributor).first + expect(latest_message.files.first.attachment).to be_attached + expect(latest_message.files.first.attachment.content_type).to eq(message[:document][:mime_type]) + end + end + + context 'audio' do + let(:document) do + { + filename: 'AUD-12345.opus', + id: 'some_valid_id', + mime_type: 'audio/ogg', + sha256: 'sha256_hash' + } + end + + before do + message[:type] = 'document' + message[:document] = document + end + + it 'attaches the file to the message with its mime_type' do + subject.call + + latest_message = Message.where(sender: contributor).first + expect(latest_message.files.first.attachment).to be_attached + expect(latest_message.files.first.attachment.content_type).to eq(message[:document][:mime_type]) + end + end + + context 'video' do + let(:document) do + { + filename: 'VID_12345.mp4', + id: 'some_valid_id', + mime_type: 'video/mp4', + sha256: 'sha256_hash' + } + end + + before do + message[:type] = 'document' + message[:document] = document + end + + it 'attaches the file to the message with its mime_type' do + subject.call + + latest_message = Message.where(sender: contributor).first + expect(latest_message.files.first.attachment).to be_attached + expect(latest_message.files.first.attachment.content_type).to eq(message[:document][:mime_type]) + end + end + end end context 'unsupported content' do @@ -393,9 +467,9 @@ context 'document' do let(:document) do { - filename: 'animated-cat-image-0056.gif', + filename: 'Comprovante.pdf', id: 'some_valid_id', - mime_type: 'image/gif', + mime_type: 'application/pdf', sha256: 'sha256_hash' } end From b60db3a52f3f916c0c74ced31f86c10ab783952e Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 23 Aug 2023 19:03:00 +0200 Subject: [PATCH 40/57] Disable lint for now --- app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb index c78d495b8..ec973af1e 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_inbound.rb @@ -59,6 +59,7 @@ def initialize_sender(whats_app_message) sender end + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def initialize_message(whats_app_message) message = whats_app_message[:messages].first text = message[:text]&.dig(:body) || message[:button]&.dig(:text) || supported_file(message)&.dig(:caption) @@ -76,6 +77,7 @@ def initialize_message(whats_app_message) ) message end + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def initialize_unsupported_content(whats_app_message) return unless unsupported_content?(whats_app_message) From 176fbf31fe22224107bfba01ebb35423c34f62d5 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 23 Aug 2023 19:04:17 +0200 Subject: [PATCH 41/57] Return class from helper method instead of substring - follow @roschaefer PR suggestion and simplify the code --- app/adapters/whats_app_adapter/outbound.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index abc03b47b..a525ac502 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -4,25 +4,25 @@ module WhatsAppAdapter class Outbound class << self def send!(message) - "WhatsAppAdapter::#{business_solution_provider}Outbound".constantize.send!(message) + business_solution_provider.constantize.send!(message) end def send_welcome_message!(contributor) - "WhatsAppAdapter::#{business_solution_provider}Outbound".constantize.send_welcome_message!(contributor) + business_solution_provider.constantize.send_welcome_message!(contributor) end def send_more_info_message!(contributor) - "WhatsAppAdapter::#{business_solution_provider}Outbound".constantize.send_more_info_message!(contributor) + business_solution_provider.constantize.send_more_info_message!(contributor) end def send_unsubsribed_successfully_message!(contributor) - "WhatsAppAdapter::#{business_solution_provider}Outbound".constantize.send_unsubsribed_successfully_message!(contributor) + business_solution_provider.constantize.send_unsubsribed_successfully_message!(contributor) end private def business_solution_provider - Setting.three_sixty_dialog_client_api_key.present? ? 'ThreeSixtyDialog' : 'Twilio' + Setting.three_sixty_dialog_client_api_key.present? ? WhatsAppAdapter::ThreeSixtyDialogOutbound : WhatsAppAdapter::TwilioOutbound end end end From a2c9f73cf4e70fedbcb55206df13791b81938753 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 24 Aug 2023 17:55:23 +0200 Subject: [PATCH 42/57] Remove redundant constantize method --- app/adapters/whats_app_adapter/outbound.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index a525ac502..67e3e81bf 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -4,19 +4,19 @@ module WhatsAppAdapter class Outbound class << self def send!(message) - business_solution_provider.constantize.send!(message) + business_solution_provider.send!(message) end def send_welcome_message!(contributor) - business_solution_provider.constantize.send_welcome_message!(contributor) + business_solution_provider.send_welcome_message!(contributor) end def send_more_info_message!(contributor) - business_solution_provider.constantize.send_more_info_message!(contributor) + business_solution_provider.send_more_info_message!(contributor) end def send_unsubsribed_successfully_message!(contributor) - business_solution_provider.constantize.send_unsubsribed_successfully_message!(contributor) + business_solution_provider.send_unsubsribed_successfully_message!(contributor) end private From 89ae62deb171acd9e27733349358f5aa0f70ba07 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 24 Aug 2023 18:51:35 +0200 Subject: [PATCH 43/57] Fix broken specs after adding contact person to org factory --- spec/factories/organizations.rb | 2 +- spec/models/contributor_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb index 4396da69a..0c64c8694 100644 --- a/spec/factories/organizations.rb +++ b/spec/factories/organizations.rb @@ -4,7 +4,7 @@ factory :organization do name { '100eyes' } upgrade_discount { 10 } - contact_person { create(:user) } + association :contact_person, factory: :user transient do users_count { 0 } diff --git a/spec/models/contributor_spec.rb b/spec/models/contributor_spec.rb index bcb841e42..4a5f71907 100644 --- a/spec/models/contributor_spec.rb +++ b/spec/models/contributor_spec.rb @@ -6,7 +6,7 @@ let(:the_request) do create(:request, title: 'Hitchhiker’s Guide', text: 'What is the answer to life, the universe, and everything?') end - let(:contributor) { create(:contributor, email: 'contributor@example.org') } + let!(:contributor) { create(:contributor, email: 'contributor@example.org') } it 'is sorted in alphabetical order' do zora = create(:contributor, first_name: 'Zora', last_name: 'Zimmermann') From 4915acd8d503a2b16201ff258ef751a593b897a3 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 24 Aug 2023 20:02:15 +0200 Subject: [PATCH 44/57] Revert "Fix broken specs after adding contact person to org factory" This reverts commit 89ae62deb171acd9e27733349358f5aa0f70ba07. --- spec/factories/organizations.rb | 2 +- spec/models/contributor_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb index 0c64c8694..4396da69a 100644 --- a/spec/factories/organizations.rb +++ b/spec/factories/organizations.rb @@ -4,7 +4,7 @@ factory :organization do name { '100eyes' } upgrade_discount { 10 } - association :contact_person, factory: :user + contact_person { create(:user) } transient do users_count { 0 } diff --git a/spec/models/contributor_spec.rb b/spec/models/contributor_spec.rb index 4a5f71907..bcb841e42 100644 --- a/spec/models/contributor_spec.rb +++ b/spec/models/contributor_spec.rb @@ -6,7 +6,7 @@ let(:the_request) do create(:request, title: 'Hitchhiker’s Guide', text: 'What is the answer to life, the universe, and everything?') end - let!(:contributor) { create(:contributor, email: 'contributor@example.org') } + let(:contributor) { create(:contributor, email: 'contributor@example.org') } it 'is sorted in alphabetical order' do zora = create(:contributor, first_name: 'Zora', last_name: 'Zimmermann') From b1414943b4bfe4610f4ef0863a10b861fa1f7c0f Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 24 Aug 2023 20:07:05 +0200 Subject: [PATCH 45/57] Fix spec without breaking other specs - I added the automatic creation of a User as a contact person, when this is actually optional. We always create a contact person for an organization though, so maybe we should consider making this change in a different PR --- spec/factories/organizations.rb | 1 - spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb index 4396da69a..c089f1ff7 100644 --- a/spec/factories/organizations.rb +++ b/spec/factories/organizations.rb @@ -4,7 +4,6 @@ factory :organization do name { '100eyes' } upgrade_discount { 10 } - contact_person { create(:user) } transient do users_count { 0 } diff --git a/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb b/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb index bded2eb8c..1a016b0f6 100644 --- a/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb +++ b/spec/requests/whats_app/three_sixty_dialog_webhook_spec.rb @@ -459,6 +459,8 @@ end context 'unsupported content' do + let(:contact_person) { create(:user) } + let!(:organization) { create(:organization, contact_person: contact_person, contributors: [contributor]) } let(:text) do I18n.t('adapter.whats_app.unsupported_content_template', first_name: contributor.first_name, contact_person: contributor.organization.contact_person.name) From f5182174316c5f552cc85f4050a390bc5523c438 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 24 Aug 2023 20:33:39 +0200 Subject: [PATCH 46/57] Lint --- app/adapters/whats_app_adapter/outbound.rb | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/app/adapters/whats_app_adapter/outbound.rb b/app/adapters/whats_app_adapter/outbound.rb index 67e3e81bf..6aac0f41a 100644 --- a/app/adapters/whats_app_adapter/outbound.rb +++ b/app/adapters/whats_app_adapter/outbound.rb @@ -3,21 +3,13 @@ module WhatsAppAdapter class Outbound class << self - def send!(message) - business_solution_provider.send!(message) - end + delegate :send!, to: :business_solution_provider - def send_welcome_message!(contributor) - business_solution_provider.send_welcome_message!(contributor) - end + delegate :send_welcome_message!, to: :business_solution_provider - def send_more_info_message!(contributor) - business_solution_provider.send_more_info_message!(contributor) - end + delegate :send_more_info_message!, to: :business_solution_provider - def send_unsubsribed_successfully_message!(contributor) - business_solution_provider.send_unsubsribed_successfully_message!(contributor) - end + delegate :send_unsubsribed_successfully_message!, to: :business_solution_provider private From d226e70daf756fccf8df2b32fcd4e819a1169daa Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 24 Aug 2023 20:33:51 +0200 Subject: [PATCH 47/57] Add spec for WhatsAppAdapter::Outbound class --- .../whats_app_adapter/outbound_spec.rb | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 spec/adapters/whats_app_adapter/outbound_spec.rb diff --git a/spec/adapters/whats_app_adapter/outbound_spec.rb b/spec/adapters/whats_app_adapter/outbound_spec.rb new file mode 100644 index 000000000..b5fb19550 --- /dev/null +++ b/spec/adapters/whats_app_adapter/outbound_spec.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe WhatsAppAdapter::Outbound do + let(:adapter) { described_class.new } + let!(:message) { create(:message, text: 'Tell me your favorite color, and why.', broadcasted: true, recipient: contributor) } + let(:contributor) { create(:contributor, email: nil) } + + describe '::send!' do + subject { -> { described_class.send!(message) } } + + context 'with 360dialog configured' do + before do + allow(Setting).to receive(:three_sixty_dialog_client_api_key).and_return('valid_api_key') + allow(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send!) + allow(WhatsAppAdapter::TwilioOutbound).to receive(:send!) + end + + it 'it is expected to send the message with 360dialog' do + expect(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send!).with(message) + + subject.call + end + + it 'it is expected not to send it with Twilio' do + expect(WhatsAppAdapter::TwilioOutbound).not_to receive(:send!) + + subject.call + end + end + + context 'without 360dialog configured' do + before do + allow(Setting).to receive(:three_sixty_dialog_client_api_key).and_return(nil) + allow(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send!) + allow(WhatsAppAdapter::TwilioOutbound).to receive(:send!) + end + + it 'it is expected not to send the message with 360dialog' do + expect(WhatsAppAdapter::ThreeSixtyDialogOutbound).not_to receive(:send!) + + subject.call + end + + it 'it is expected to send it with Twilio' do + expect(WhatsAppAdapter::TwilioOutbound).to receive(:send!).with(message) + + subject.call + end + end + end + + describe '::send_welcome_message!' do + subject { -> { described_class.send_welcome_message!(contributor) } } + + context 'with 360dialog configured' do + before do + allow(Setting).to receive(:three_sixty_dialog_client_api_key).and_return('valid_api_key') + allow(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send_welcome_message!) + allow(WhatsAppAdapter::TwilioOutbound).to receive(:send_welcome_message!) + end + + it 'it is expected to send the welcome message with 360dialog' do + expect(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send_welcome_message!).with(contributor) + + subject.call + end + + it 'it is expected not to send the welcome message with Twilio' do + expect(WhatsAppAdapter::TwilioOutbound).not_to receive(:send_welcome_message!) + + subject.call + end + end + + context 'without 360dialog configured' do + before do + allow(Setting).to receive(:three_sixty_dialog_client_api_key).and_return(nil) + allow(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send_welcome_message!) + allow(WhatsAppAdapter::TwilioOutbound).to receive(:send_welcome_message!) + end + + it 'it is expected not to send the welcome message with 360dialog' do + expect(WhatsAppAdapter::ThreeSixtyDialogOutbound).not_to receive(:send_welcome_message!) + + subject.call + end + + it 'it is expected to send the welcome message with Twilio' do + expect(WhatsAppAdapter::TwilioOutbound).to receive(:send_welcome_message!).with(contributor) + + subject.call + end + end + end + + describe '::send_more_info_message!' do + subject { -> { described_class.send_more_info_message!(contributor) } } + + context 'with 360dialog configured' do + before do + allow(Setting).to receive(:three_sixty_dialog_client_api_key).and_return('valid_api_key') + allow(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send_more_info_message!) + allow(WhatsAppAdapter::TwilioOutbound).to receive(:send_more_info_message!) + end + + it 'it is expected to send the more info message with 360dialog' do + expect(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send_more_info_message!).with(contributor) + + subject.call + end + + it 'it is expected not to send the more info message with Twilio' do + expect(WhatsAppAdapter::TwilioOutbound).not_to receive(:send_more_info_message!) + + subject.call + end + end + + context 'without 360dialog configured' do + before do + allow(Setting).to receive(:three_sixty_dialog_client_api_key).and_return(nil) + allow(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send_more_info_message!) + allow(WhatsAppAdapter::TwilioOutbound).to receive(:send_more_info_message!) + end + + it 'it is expected not to send the more info message with 360dialog' do + expect(WhatsAppAdapter::ThreeSixtyDialogOutbound).not_to receive(:send_more_info_message!) + + subject.call + end + + it 'it is expected to send the more info message with Twilio' do + expect(WhatsAppAdapter::TwilioOutbound).to receive(:send_more_info_message!).with(contributor) + + subject.call + end + end + end + + describe '::send_unsubsribed_successfully_message!' do + subject { -> { described_class.send_unsubsribed_successfully_message!(contributor) } } + + context 'with 360dialog configured' do + before do + allow(Setting).to receive(:three_sixty_dialog_client_api_key).and_return('valid_api_key') + allow(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send_unsubsribed_successfully_message!) + allow(WhatsAppAdapter::TwilioOutbound).to receive(:send_unsubsribed_successfully_message!) + end + + it 'it is expected to send the unsubscribed successfully message with 360dialog' do + expect(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send_unsubsribed_successfully_message!).with(contributor) + + subject.call + end + + it 'it is expected not to send the unsubscribed successfully message with Twilio' do + expect(WhatsAppAdapter::TwilioOutbound).not_to receive(:send_unsubsribed_successfully_message!) + + subject.call + end + end + + context 'without 360dialog configured' do + before do + allow(Setting).to receive(:three_sixty_dialog_client_api_key).and_return(nil) + allow(WhatsAppAdapter::ThreeSixtyDialogOutbound).to receive(:send_unsubsribed_successfully_message!) + allow(WhatsAppAdapter::TwilioOutbound).to receive(:send_unsubsribed_successfully_message!) + end + + it 'it is expected to send the unsubscribed successfully message with 360dialog' do + expect(WhatsAppAdapter::ThreeSixtyDialogOutbound).not_to receive(:send_unsubsribed_successfully_message!) + + subject.call + end + + it 'it is expected not to send the unsubscribed successfully message with Twilio' do + expect(WhatsAppAdapter::TwilioOutbound).to receive(:send_unsubsribed_successfully_message!).with(contributor) + + subject.call + end + end + end +end From da959e1e34d9c19e7ebc33d2e146bd0785ae7c72 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 24 Aug 2023 21:42:13 +0200 Subject: [PATCH 48/57] Send out "random" templates --- app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb b/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb index 1233f0726..d1f24da0a 100644 --- a/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb +++ b/app/adapters/whats_app_adapter/three_sixty_dialog_outbound.rb @@ -112,7 +112,7 @@ def new_request_payload(recipient, request) policy: 'deterministic', code: 'de' }, - name: 'new_request_morning_1', # TODO: Use dynamic template name after WhatsAppAdapter::CreateTemplate works + name: "new_request_#{time_of_day}_#{rand(1..3)}", components: [ { type: 'body', From 3f15b55b06fed1678328e36bfbd95983ce2f7f40 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 24 Aug 2023 21:43:04 +0200 Subject: [PATCH 49/57] Remove unused .env variables from template --- .env.template | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.env.template b/.env.template index f360e9538..ccf6bd9ab 100644 --- a/.env.template +++ b/.env.template @@ -27,6 +27,4 @@ THREEMARB_PRIVATE= # # THREE_SIXTY_DIALOG_PARTNER_ID= # THREE_SIXTY_DIALOG_PARTNER_USERNAME= -# THREE_SIXTY_DIALOG_PARTNER_PASSWORD= -# THREE_SIXTY_DIALOG_WHATS_APP_REST_API_ENDPOINT= -# THREE_SIXTY_DIALOG_PARTNER_REST_API_ENDPOINT= \ No newline at end of file +# THREE_SIXTY_DIALOG_PARTNER_PASSWORD= \ No newline at end of file From e90e1c13782c9f8fd3278b99b6f6f3b8f0ea2548 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 24 Aug 2023 21:43:47 +0200 Subject: [PATCH 50/57] Add spec for 360dialog outbound .send! method --- .../three_sixty_dialog_outbound_spec.rb | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 spec/adapters/whats_app_adapter/three_sixty_dialog_outbound_spec.rb diff --git a/spec/adapters/whats_app_adapter/three_sixty_dialog_outbound_spec.rb b/spec/adapters/whats_app_adapter/three_sixty_dialog_outbound_spec.rb new file mode 100644 index 000000000..4d0aedd78 --- /dev/null +++ b/spec/adapters/whats_app_adapter/three_sixty_dialog_outbound_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe WhatsAppAdapter::ThreeSixtyDialogOutbound do + let(:adapter) { described_class.new } + let!(:message) { create(:message, text: '360dialog is great!', broadcasted: true, recipient: contributor) } + let(:contributor) { create(:contributor, email: nil) } + let(:new_request_payload) do + { + recipient_type: 'individual', + to: contributor.whats_app_phone_number.split('+').last, + type: 'template', + template: { + namespace: Setting.three_sixty_dialog_whats_app_template_namespace, + language: { + policy: 'deterministic', + code: 'de' + }, + name: kind_of(String), + components: [ + { + type: 'body', + parameters: [ + { + type: 'text', + text: contributor.first_name + }, + { + type: 'text', + text: message.request.title + } + ] + } + ] + } + } + end + let(:text_payload) do + { + recipient_type: 'individual', + to: contributor.whats_app_phone_number.split('+').last, + type: 'text', + text: { + body: message.text + } + } + end + + describe '::send!' do + subject { -> { described_class.send!(message) } } + before { message } # we don't count the extra ::send here + + context '`whats_app_phone_number` blank' do + it { should_not enqueue_job(WhatsAppAdapter::Outbound::Text) } + end + + context 'given a WhatsApp contributor' do + let(:contributor) do + create( + :contributor, + email: nil, + whats_app_phone_number: '+491511234567' + ) + end + + describe 'contributor has not sent a message within 24 hours' do + it 'enqueues the Text job with WhatsApp template' do + expect { subject.call }.to(have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).on_queue('default').with do |params| + expect(params[:payload]).to include(new_request_payload) + end) + end + end + + describe 'contributor has responded to a template' do + before { contributor.update(whats_app_message_template_responded_at: Time.current) } + + it 'enqueues the Text job with the request text' do + expect { subject.call }.to(have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).on_queue('default').with do |params| + expect(params[:payload]).to eq(text_payload) + end) + end + end + + describe 'contributor has sent a reply within 24 hours' do + before { create(:message, sender: contributor) } + + it 'enqueues the Text job with the request text' do + expect { subject.call }.to(have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).on_queue('default').with do |params| + expect(params[:payload]).to eq(text_payload) + end) + end + end + + describe 'message with files' do + let(:file) { create(:file) } + before { message.update(files: [file]) } + + context 'contributor has not sent a message within 24 hours' do + it 'enqueues the Text job with WhatsApp template' do + expect do + subject.call + end.to(have_enqueued_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).on_queue('default').with do |params| + expect(params[:payload]).to include(new_request_payload) + end) + end + end + + context 'contributor has sent a reply within 24 hours' do + before { create(:message, sender: contributor) } + it 'enqueues a File job with file, contributor, text' do + expect { subject.call }.to(have_enqueued_job(WhatsAppAdapter::UploadFile).on_queue('default').with do |params| + expect(params[:message_id]).to eq(message.id) + end) + end + end + end + end + end +end From bbcbfed477efbc5b6650454d717fe79782fe856a Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 28 Aug 2023 19:59:53 +0200 Subject: [PATCH 51/57] Initialize three sixty dialog settings with default empty string - we experienced buggy behavior when the setting was intialized in a job where the value was not accessible to be queried using the syntax sugar Setting.three_sixty_dialog_client_api_key. Initializing with an empty string means the setting gets created on intialization of the server and we didn't run into an issue updating the value and accessing it straight away. --- app/models/setting.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/setting.rb b/app/models/setting.rb index 43310a363..b0b4ee64b 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -72,15 +72,15 @@ def self.onboarding_hero=(blob) field :twilio_api_key_secret, readonly: true, default: ENV.fetch('TWILIO_API_KEY_SECRET', nil) field :whats_app_server_phone_number, readonly: true, default: ENV.fetch('WHATS_APP_SERVER_PHONE_NUMBER', nil) - field :three_sixty_dialog_partner_token + field :three_sixty_dialog_partner_token, default: '' field :three_sixty_dialog_partner_id, readonly: true, default: ENV.fetch('THREE_SIXTY_DIALOG_PARTNER_ID', nil) field :three_sixty_dialog_partner_username, readonly: true, default: ENV.fetch('THREE_SIXTY_DIALOG_PARTNER_USERNAME', nil) field :three_sixty_dialog_partner_password, readonly: true, default: ENV.fetch('THREE_SIXTY_DIALOG_PARTNER_PASSWORD', nil) field :three_sixty_dialog_partner_rest_api_endpoint, readonly: true, default: ENV.fetch('THREE_SIXTY_DIALOG_PARTNER_REST_API_ENDPOINT', 'https://stoplight.io/mocks/360dialog/360dialog-partner-api/24588693') - field :three_sixty_dialog_client_api_key - field :three_sixty_dialog_client_id - field :three_sixty_dialog_client_waba_account_id + field :three_sixty_dialog_client_api_key, default: '' + field :three_sixty_dialog_client_id, default: '' + field :three_sixty_dialog_client_waba_account_id, default: '' field :three_sixty_dialog_whats_app_rest_api_endpoint, readonly: true, default: ENV.fetch('THREE_SIXTY_DIALOG_WHATS_APP_REST_API_ENDPOINT', 'https://waba-sandbox.360dialog.io') From faf7e42ab5332f5b1e18822d5f66bc70e6dcc52a Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Mon, 28 Aug 2023 20:46:41 +0200 Subject: [PATCH 52/57] Finish writing outbound spec for 360dialog --- .../three_sixty_dialog_outbound_spec.rb | 159 +++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/spec/adapters/whats_app_adapter/three_sixty_dialog_outbound_spec.rb b/spec/adapters/whats_app_adapter/three_sixty_dialog_outbound_spec.rb index 4d0aedd78..dd5c3fe6d 100644 --- a/spec/adapters/whats_app_adapter/three_sixty_dialog_outbound_spec.rb +++ b/spec/adapters/whats_app_adapter/three_sixty_dialog_outbound_spec.rb @@ -47,12 +47,39 @@ } end + let(:welcome_message_payload) do + { + recipient_type: 'individual', + to: contributor.whats_app_phone_number.split('+').last, + type: 'template', + template: { + namespace: Setting.three_sixty_dialog_whats_app_template_namespace, + language: { + policy: 'deterministic', + code: 'de' + }, + name: 'welcome_message', + components: [ + { + type: 'body', + parameters: [ + { + type: 'text', + text: Setting.project_name + } + ] + } + ] + } + } + end + describe '::send!' do subject { -> { described_class.send!(message) } } before { message } # we don't count the extra ::send here context '`whats_app_phone_number` blank' do - it { should_not enqueue_job(WhatsAppAdapter::Outbound::Text) } + it { should_not enqueue_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText) } end context 'given a WhatsApp contributor' do @@ -108,6 +135,7 @@ context 'contributor has sent a reply within 24 hours' do before { create(:message, sender: contributor) } + it 'enqueues a File job with file, contributor, text' do expect { subject.call }.to(have_enqueued_job(WhatsAppAdapter::UploadFile).on_queue('default').with do |params| expect(params[:message_id]).to eq(message.id) @@ -117,4 +145,133 @@ end end end + + describe '#send_welcome_message!' do + subject { -> { described_class.send_welcome_message!(contributor) } } + + it { is_expected.not_to enqueue_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText) } + + context 'contributor has a phone number' do + let(:contributor) do + create( + :contributor, + whats_app_phone_number: '+491511234567', + email: nil + ) + end + + context 'and no replies sent(new contributor)' do + it { is_expected.to enqueue_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).with({ payload: welcome_message_payload }) } + end + + context 'with replies sent within 24 hours' do + before do + create(:message, sender: contributor) + text_payload[:text][:body] = I18n.t('adapter.whats_app.welcome_message', project_name: Setting.project_name) + end + + it { is_expected.to enqueue_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).with({ payload: text_payload }) } + end + end + end + + describe '#send_unsupported_content_message!' do + subject { -> { described_class.send_unsupported_content_message!(contributor) } } + + it { is_expected.not_to enqueue_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText) } + + context 'contributor has a phone number' do + let(:contributor) do + create( + :contributor, + whats_app_phone_number: '+491511234567', + email: nil, + organization: organization + ) + end + let(:contact_person) { create(:user) } + let(:organization) { create(:organization, contact_person: contact_person) } + + before do + text_payload[:text][:body] = I18n.t('adapter.whats_app.unsupported_content_template', + first_name: contributor.first_name, + contact_person: contributor.organization.contact_person.name) + end + + it { is_expected.to enqueue_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).with({ payload: text_payload }) } + end + end + + describe '#send_more_info_message!' do + subject { -> { described_class.send_more_info_message!(contributor) } } + + it { is_expected.not_to enqueue_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText) } + + context 'contributor has a phone number' do + let(:contributor) do + create( + :contributor, + whats_app_phone_number: '+491511234567', + email: nil + ) + end + + before do + text_payload[:text][:body] = [Setting.about, "_#{I18n.t('adapter.whats_app.unsubscribe.instructions')}_"].join("\n\n") + end + + it { is_expected.to enqueue_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).with({ payload: text_payload }) } + end + end + + describe '#send_unsubsribed_successfully_message!' do + subject { -> { described_class.send_unsubsribed_successfully_message!(contributor) } } + + it { is_expected.not_to enqueue_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText) } + + context 'contributor has a phone number' do + let(:contributor) do + create( + :contributor, + whats_app_phone_number: '+491511234567', + email: nil + ) + end + + before do + text_payload[:text][:body] = [I18n.t('adapter.whats_app.unsubscribe.successful'), + "_#{I18n.t('adapter.whats_app.subscribe.instructions')}_"].join("\n\n") + end + + it { is_expected.to enqueue_job(WhatsAppAdapter::Outbound::ThreeSixtyDialogText).with({ payload: text_payload }) } + end + end + + describe '::freeform_message_permitted?(recipient)' do + subject { described_class.send(:freeform_message_permitted?, contributor) } + + describe 'template message' do + context 'contributor has responded' do + before { contributor.update(whats_app_message_template_responded_at: 1.second.ago) } + + it { is_expected.to eq(true) } + end + + context 'contributor has not responded, and has no messages within 24 hours' do + it { is_expected.to eq(false) } + end + end + + describe 'message from contributor within 24 hours' do + context 'has been received' do + before { create(:message, sender: contributor) } + + it { is_expected.to eq(true) } + end + + context 'has not been received' do + it { is_expected.to eq(false) } + end + end + end end From 691a1f442200837c0f1c159bba25c54b01ad1733 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 31 Aug 2023 13:02:37 +0200 Subject: [PATCH 53/57] Remove unrelated code changes --- .../profile_contributors_section.html.erb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/components/profile_contributors_section/profile_contributors_section.html.erb b/app/components/profile_contributors_section/profile_contributors_section.html.erb index e432a9c98..23c64ef83 100644 --- a/app/components/profile_contributors_section/profile_contributors_section.html.erb +++ b/app/components/profile_contributors_section/profile_contributors_section.html.erb @@ -2,12 +2,10 @@ <%= c 'heading', style: :beta do %> <%= t('profile.contributors_section.main_heading') %> <% end %> -

- <%= t('profile.contributors_section.number_of_contributors_of_business_plan_used', - contributors_count: organization.contributors.active.count, - allocated_contributors: organization.business_plan.number_of_contributors) +

<%= t('profile.contributors_section.number_of_contributors_of_business_plan_used', + contributors_count: organization.contributors.active.count, + allocated_contributors: organization.business_plan.number_of_contributors) %> -

<%= c 'flex' do %> <%= c 'contributors_status_bar', organization: organization %> <%= c 'copy_button', From bd940c84e35bf3cfd742efbcc5bac68e9258ab62 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 31 Aug 2023 13:15:12 +0200 Subject: [PATCH 54/57] Remove console log --- app/components/whats_app_setup/whats_app_setup.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/components/whats_app_setup/whats_app_setup.js b/app/components/whats_app_setup/whats_app_setup.js index eb63875c1..82b2be0c6 100644 --- a/app/components/whats_app_setup/whats_app_setup.js +++ b/app/components/whats_app_setup/whats_app_setup.js @@ -5,10 +5,6 @@ export default class extends Controller { permissionsUrl: String, }; - connect() { - console.log('connected'); - } - openModal() { const windowFeatures = 'toolbar=no, menubar=no, width=600, height=900, top=100, left=100'; From 1007c3877be2c0a2c34c70d0fdb6f0970af90ce2 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Thu, 31 Aug 2023 13:15:25 +0200 Subject: [PATCH 55/57] Namespace concern to be more specific --- app/controllers/concerns/handle_callbacks.rb | 2 +- .../whats_app/three_sixty_dialog_webhook_controller.rb | 2 +- app/controllers/whats_app/webhook_controller.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/concerns/handle_callbacks.rb b/app/controllers/concerns/handle_callbacks.rb index 11e94135b..117f6ec41 100644 --- a/app/controllers/concerns/handle_callbacks.rb +++ b/app/controllers/concerns/handle_callbacks.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module HandleCallbacks +module WhatsAppHandleCallbacks extend ActiveSupport::Concern private diff --git a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb index 3af62ebda..10efd86fc 100644 --- a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb +++ b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb @@ -2,7 +2,7 @@ module WhatsApp class ThreeSixtyDialogWebhookController < ApplicationController - include HandleCallbacks + include WhatsAppHandleCallbacks skip_before_action :require_login, :verify_authenticity_token diff --git a/app/controllers/whats_app/webhook_controller.rb b/app/controllers/whats_app/webhook_controller.rb index b72f13690..ee5dc94a3 100644 --- a/app/controllers/whats_app/webhook_controller.rb +++ b/app/controllers/whats_app/webhook_controller.rb @@ -2,7 +2,7 @@ module WhatsApp class WebhookController < ApplicationController - include HandleCallbacks + include WhatsAppHandleCallbacks skip_before_action :require_login, :verify_authenticity_token UNSUCCESSFUL_DELIVERY = %w[undelivered failed].freeze From 02c6d4121e36727248fba333f83d7de2624ffc1e Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 6 Sep 2023 16:38:41 +0200 Subject: [PATCH 56/57] Reorganize strong params for readability --- .../three_sixty_dialog_webhook_controller.rb | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb index 10efd86fc..c1870d387 100644 --- a/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb +++ b/app/controllers/whats_app/three_sixty_dialog_webhook_controller.rb @@ -56,26 +56,22 @@ def create_api_key private def message_params - params.permit({ three_sixty_dialog_webhook: [contacts: [:wa_id, { profile: [:name] }], - messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, - { button: [:text] }, { image: %i[id mime_type sha256 caption] }, - { voice: %i[id mime_type sha256] }, - { video: %i[id mime_type sha256 caption] }, - { audio: %i[id mime_type sha256] }, - { errors: %i[code details title] }, - { document: %i[filename id mime_type sha256] }, - { location: %i[latitude longitude] }, - { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, - { phones: %i[phone type wa_id] }, { urls: [] }, - { name: %i[first_name formatted_name last_name] }] }]] }, + params.permit({ three_sixty_dialog_webhook: + [contacts: [:wa_id, { profile: [:name] }], + messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, + { button: [:text] }, { image: %i[id mime_type sha256 caption] }, + { voice: %i[id mime_type sha256] }, { video: %i[id mime_type sha256 caption] }, + { audio: %i[id mime_type sha256] }, { errors: %i[code details title] }, + { document: %i[filename id mime_type sha256] }, { location: %i[latitude longitude] }, + { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, + { phones: %i[phone type wa_id] }, { urls: [] }, + { name: %i[first_name formatted_name last_name] }] }]] }, contacts: [:wa_id, { profile: [:name] }], - messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, { button: [:text] }, - { image: %i[id mime_type sha256 caption] }, { voice: %i[id mime_type sha256] }, - { video: %i[id mime_type sha256 caption] }, - { audio: %i[id mime_type sha256] }, - { errors: %i[code details title] }, - { document: %i[filename id mime_type sha256] }, - { location: %i[latitude longitude] }, + messages: [:from, :id, :type, :timestamp, { text: [:body] }, { context: %i[from id] }, + { button: [:text] }, { image: %i[id mime_type sha256 caption] }, + { voice: %i[id mime_type sha256] }, { video: %i[id mime_type sha256 caption] }, + { audio: %i[id mime_type sha256] }, { errors: %i[code details title] }, + { document: %i[filename id mime_type sha256] }, { location: %i[latitude longitude] }, { contacts: [{ org: {} }, { addresses: [] }, { emails: [] }, { ims: [] }, { phones: %i[phone type wa_id] }, { urls: [] }, { name: %i[first_name formatted_name last_name] }] }]) From b3b7681141b72327f02b0b00baf66ac92b2e17e4 Mon Sep 17 00:00:00 2001 From: mattwr18 Date: Wed, 6 Sep 2023 16:58:29 +0200 Subject: [PATCH 57/57] Rename file after renaming concern --- .../{handle_callbacks.rb => whats_app_handle_callbacks.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/controllers/concerns/{handle_callbacks.rb => whats_app_handle_callbacks.rb} (100%) diff --git a/app/controllers/concerns/handle_callbacks.rb b/app/controllers/concerns/whats_app_handle_callbacks.rb similarity index 100% rename from app/controllers/concerns/handle_callbacks.rb rename to app/controllers/concerns/whats_app_handle_callbacks.rb