Skip to content

Commit

Permalink
Fixes #5010 - WhatsApp Business Channel (first iteration)
Browse files Browse the repository at this point in the history
Co-authored-by: Benjamin Scharf <bs@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Mantas Masalskis <mm@zammad.com>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Tobias Schäfer <ts@zammad.com>
  • Loading branch information
7 people committed Feb 20, 2024
1 parent 40cc955 commit 1ef8c23
Show file tree
Hide file tree
Showing 67 changed files with 2,773 additions and 10 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ gem 'rack-attack'
gem 'koala'
gem 'telegram-bot-ruby'
gem 'twitter'
gem 'whatsapp_sdk'

# channels - email additions
gem 'email_address'
Expand Down
7 changes: 7 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,7 @@ GEM
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
sorbet-runtime (0.5.11255)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
Expand Down Expand Up @@ -699,6 +700,11 @@ GEM
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
whatsapp_sdk (0.11.0)
faraday (~> 2)
faraday-multipart (~> 1)
sorbet-runtime (~> 0.5)
zeitwerk (~> 2)
write_xlsx (1.11.2)
nkf
rubyzip (>= 1.0.0)
Expand Down Expand Up @@ -838,6 +844,7 @@ DEPENDENCIES
vite_rails
webauthn
webmock
whatsapp_sdk
write_xlsx
zendesk_api

Expand Down
5 changes: 5 additions & 0 deletions LICENSE-ICONS-3RD-PARTY.json
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,11 @@
"url": "https:\/\/thenounproject.com\/search\/?q=user&i=10314",
"license": "CC 3.0 Attribution"
},
"whatsapp.svg": {
"author": "Meta",
"url": "https://about.meta.com/uk/brand/resources/whatsapp/whatsapp-brand/",
"license": ""
},
"web.svg": {
"author": "Zammad",
"url": "",
Expand Down
270 changes: 270 additions & 0 deletions app/assets/javascripts/app/controllers/_channel/whatsapp.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
class ChannelWhatsapp extends App.ControllerSubContent
@requiredPermission: 'admin.channel_whatsapp'
events:
'click .js-new': 'new'
'click .js-edit': 'edit'
'click .js-delete': 'delete'
'click .js-disable': 'disable'
'click .js-enable': 'enable'

constructor: ->
super

@load()

load: =>
@startLoading()
@ajax(
id: 'whatsapp_index'
type: 'GET'
url: "#{@apiPath}/channels/admin/whatsapp"
processData: true
success: (data) =>
@stopLoading()
App.Collection.loadAssets(data.assets)
@render(data)
)

render: (data) =>
channels = data.channel_ids.map (elem) -> App.Channel.find(elem)

@html App.view('whatsapp/index')(
channels: channels
)

new: (e) =>
e.preventDefault()

new WhatsappAccountCloudAPIModal(
container: @el.parents('.content')
load: @load
headPrefix: __('New')
)

edit: (e) =>
e.preventDefault()

id = $(e.target).closest('.action').data('id')
channel = App.Channel.find(id)

new WhatsappAccountCloudAPIModal(
container: @el.parents('.content')
channel: channel
load: @load
headPrefix: __('Edit')
)

delete: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')

new App.ControllerConfirm(
message: __('Are you sure?')
buttonClass: 'btn--danger'
callback: =>
@ajax(
id: 'whatsapp_delete'
type: 'DELETE'
url: "#{@apiPath}/channels/admin/whatsapp/#{id}"
processData: true
success: =>
@load()
)
container: @el.closest('.content')
)

disable: (e) =>
e.preventDefault()

id = $(e.target).closest('.action').data('id')

@ajax(
id: 'whatsapp_disable'
type: 'POST'
url: "#{@apiPath}/channels/admin/whatsapp/#{id}/disable"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)

enable: (e) =>
e.preventDefault()

id = $(e.target).closest('.action').data('id')

@ajax(
id: 'whatsapp_enable'
type: 'POST'
url: "#{@apiPath}/channels/admin/whatsapp/#{id}/enable"
processData: true
success: =>
@load()
)

class WhatsappAccountCloudAPIModal extends App.ControllerModal
head: __('WhatsApp Account')
shown: true
buttonSubmit: __('Next')
buttonClass: 'btn--primary'
buttonCancel: true
small: true

content: =>
$(App.view('whatsapp/account_cloud_api')(
channel: @channel
params: @params
))

onSubmit: (e) =>
element = $(e.target).closest('form').get(0)
if element && element.reportValidity && !element.reportValidity()
return false

@clearAlerts()
@formDisable(e)

params = if @params then _.extend(@params, @formParams()) else @formParams()

@ajax(
id: 'whatsapp_initial'
type: 'POST'
url: "#{@apiPath}/channels/admin/whatsapp/preload"
data: JSON.stringify(params)
processData: true
success: (data) =>
@el.removeClass('fade')
@close()

params.available_phone_numbers = data.data.phone_numbers

new WhatsappAccountPhoneNumberModal(
params: params
channel: @channel
container: @container
load: @load
headPrefix: @headPrefix
)
error: (xhr) =>
data = JSON.parse(xhr.responseText)
@formEnable(e)
error_message = App.i18n.translateContent(data.error || __('The WhatsApp connection could not be saved.'))
@showAlert(error_message)
)

class WhatsappAccountPhoneNumberModal extends App.ControllerModal
head: __('WhatsApp Account')
shown: true
buttonCancel: true
small: true

content: =>
content = $(App.view('whatsapp/account_phone_number')(
channel: @channel
params: @params
))

preselected_group_id = if @channel then @channel.group_id else 1

content.find('.js-messagesGroup').replaceWith App.UiElement.tree_select.render(
name: 'group_id'
multiple: false
limit: 100
null: false
relation: 'Group'
nulloption: true
value: preselected_group_id
)

content.find('.js-phoneNumbers').replaceWith App.UiElement.select.render(
name: 'phone_number_id'
multiple: false
value: @channel?.options?.phone_number_id || @params.available_phone_numbers?[0]?.value
options: @params.available_phone_numbers?.map (elem) -> { name: elem.label, value: elem.value }
)

content

onClosed: =>
return if !@isChanged
@isChanged = false
@load()

onSubmit: (e) =>
element = $(e.target).closest('form').get(0)
if element && element.reportValidity && !element.reportValidity()
return false

@clearAlerts()

if @channel
url = "#{@apiPath}/channels/admin/whatsapp/#{@channel.id}"
method = 'PUT'
else
url = "#{@apiPath}/channels/admin/whatsapp"
method = 'POST'

@formDisable(e)

params = @formParams()

@ajax(
id: 'whatsapp_save'
type: method
url: url
data: JSON.stringify(params)
processData: true
success: (data) =>
@isChanged = true
@el.removeClass('fade')
@close()

new WhatsappAccountWebhookModal(
channel: data
container: @container
headPrefix: @headPrefix
)
error: (xhr) =>
data = JSON.parse(xhr.responseText)
@formEnable(e)
error_message = App.i18n.translateContent(data.error || __('The WhatsApp connection could not be saved.'))
@showAlert(error_message)
)

class WhatsappAccountWebhookModal extends App.ControllerModal
head: __('WhatsApp Account')
shown: true
buttonSubmit: __('Finish')
buttonClass: 'btn--primary'
small: true
events:
'click .js-copy': 'copyToClipboard'

content: =>
content = $(App.view('whatsapp/account_webhook')(
channel: @channel
callback_url: "#{@Config.get('http_type')}://#{@Config.get('fqdn')}/#{@apiPath}/channels_whatsapp_webhook/#{@channel.options?.callback_url_uuid}"
))

content

onSubmit: (e) =>
@close()

copyToClipboard: (e) =>
e.preventDefault()

button = $(e.target).parents('[role="button"]')
field_name = button.data('targetField')
value = $(@container).find("input[name='#{jQuery.escapeSelector(field_name)}']").val()

@copyToClipboardWithTooltip(value, e.target,'.modal-body', true)

App.Config.set('Whatsapp', {
prio: 5100,
name: __('WhatsApp'),
parent: '#channels',
target: '#channels/whatsapp',
controller: ChannelWhatsapp,
permission: ['admin.channel_whatsapp']
}, 'NavBarAdmin')
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<div class="alert alert--danger hidden" role="alert"></div>
<p>
<%- @T('You can find a tutorial on how to manage a %s in our online documentation %l.', 'WhatsApp Business Account', 'https://admin-docs.zammad.org/en/latest/channels/whatsapp.html') %>
</p>
<fieldset>
<h2><%- @T('Step 1 of 3: WhatsApp Business Cloud API') %></h2>

<div class="input form-group">
<div class="formGroup-label">
<label for="business_id"><%- @T('WhatsApp Business Account ID') %><% if !@channel: %> <span>*</span><% end %></label>
</div>
<div class="controls">
<input id="business_id" type="text" name="business_id" value="<%= @params?.business_id || @channel?.options?.business_id %>" class="form-control" <% if @channel: %>disabled<% else: %>required autocomplete="off"<% end %>>
</div>
</div>

<div class="input form-group">
<div class="formGroup-label">
<label for="access_token"><%- @T('Access token') %> <span>*</span></label>
</div>
<div class="controls">
<input id="access_token" type="text" name="access_token" value="<%= @params?.access_token || @channel?.options?.access_token %>" class="form-control" required autocomplete="off">
</div>
</div>

<div class="input form-group">
<div class="formGroup-label">
<label for="app_secret"><%- @T('App secret') %> <span>*</span></label>
</div>
<div class="controls">
<input id="app_secret" type="text" name="app_secret" value="<%= @params?.app_secret || @channel?.options?.app_secret %>" class="form-control" required autocomplete="off">
</div>
</div>
</fieldset>

0 comments on commit 1ef8c23

Please sign in to comment.