Add all repos

This commit is contained in:
Darren Clarke 2023-02-13 12:41:30 +00:00
parent faa12c60bc
commit 8a91c9b89b
369 changed files with 29047 additions and 28 deletions

View file

@ -0,0 +1,249 @@
class Index extends App.ControllerSubContent
requiredPermission: 'admin.channel_cdr_signal'
events:
'click .js-new': 'new'
'click .js-edit': 'edit'
'click .js-delete': 'delete'
'click .js-disable': 'disable'
'click .js-enable': 'enable'
'click .js-rotate-token': 'rotateToken'
constructor: ->
super
#@interval(@load, 60000)
@load()
load: =>
@startLoading()
@ajax(
id: 'cdr_signal_index'
type: 'GET'
url: "#{@apiPath}/channels_cdr_signal"
processData: true
success: (data) =>
@stopLoading()
App.Collection.loadAssets(data.assets)
@render(data)
)
render: (data) =>
channels = []
for channel_id in data.channel_ids
channel = App.Channel.find(channel_id)
if channel && channel.options
displayName = '-'
if channel.group_id
group = App.Group.find(channel.group_id)
displayName = group.displayName()
channel.options.groupName = displayName
channels.push channel
@html App.view('cdr_signal/index')(
channels: channels
)
new: (e) =>
e.preventDefault()
new FormAdd(
container: @el.parents('.content')
load: @load
)
edit: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
channel = App.Channel.find(id)
new FormEdit(
channel: channel
container: @el.parents('.content')
load: @load
)
delete: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
new App.ControllerConfirm(
message: 'Sure?'
callback: =>
@ajax(
id: 'cdr_signal_delete'
type: 'DELETE'
url: "#{@apiPath}/channels_cdr_signal"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
container: @el.closest('.content')
)
rotateToken: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
new App.ControllerConfirm(
message: 'This will break the submission form!'
buttonSubmit: 'Reset token'
head: 'Reset the submission token?'
callback: =>
@ajax(
id: 'cdr_signal_disable'
type: 'POST'
url: "#{@apiPath}/channels_cdr_signal_rotate_token"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
container: @el.closest('.content')
)
disable: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
@ajax(
id: 'cdr_signal_disable'
type: 'POST'
url: "#{@apiPath}/channels_cdr_signal_disable"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
enable: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
@ajax(
id: 'cdr_signal_enable'
type: 'POST'
url: "#{@apiPath}/channels_cdr_signal_enable"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
class FormAdd extends App.ControllerModal
head: 'Add Web Form'
shown: true
button: 'Add'
buttonCancel: true
small: true
content: ->
content = $(App.view('cdr_signal/form_add')())
createOrgSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'organization_id'
multiple: false
limit: 100
null: false
relation: 'Organization'
nulloption: true
value: selected_id
class: 'form-control--small'
)
createGroupSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'group_id'
multiple: false
limit: 100
null: false
relation: 'Group'
nulloption: true
value: selected_id
class: 'form-control--small'
)
content.find('.js-select').on('click', (e) =>
@selectAll(e)
)
content.find('.js-messagesGroup').replaceWith createGroupSelection(1)
content.find('.js-organization').replaceWith createOrgSelection(null)
content
onClosed: =>
return if !@isChanged
@isChanged = false
@load()
onSubmit: (e) =>
@formDisable(e)
@ajax(
id: 'cdr_signal_app_verify'
type: 'POST'
url: "#{@apiPath}/channels_cdr_signal"
data: JSON.stringify(@formParams())
processData: true
success: =>
@isChanged = true
@close()
error: (xhr) =>
data = JSON.parse(xhr.responseText)
@formEnable(e)
error_message = App.i18n.translateContent(data.error || 'Unable to save Web Form.')
@el.find('.alert').removeClass('hidden').text(error_message)
)
class FormEdit extends App.ControllerModal
head: 'Web Form Info'
shown: true
buttonCancel: true
content: ->
content = $(App.view('cdr_signal/form_edit')(channel: @channel))
createOrgSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'organization_id'
multiple: false
limit: 100
null: false
relation: 'Organization'
nulloption: true
value: selected_id
class: 'form-control--small'
)
createGroupSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'group_id'
multiple: false
limit: 100
null: false
relation: 'Group'
nulloption: true
value: selected_id
class: 'form-control--small'
)
content.find('.js-messagesGroup').replaceWith createGroupSelection(@channel.group_id)
content.find('.js-organization').replaceWith createOrgSelection(@channel.options.organization_id)
content
onClosed: =>
return if !@isChanged
@isChanged = false
@load()
onSubmit: (e) =>
@formDisable(e)
params = @formParams()
@channel.options = params
@ajax(
id: 'channel_cdr_signal_update'
type: 'PUT'
url: "#{@apiPath}/channels_cdr_signal/#{@channel.id}"
data: JSON.stringify(@formParams())
processData: true
success: =>
@isChanged = true
@close()
error: (xhr) =>
data = JSON.parse(xhr.responseText)
@formEnable(e)
error_message = App.i18n.translateContent(data.error || 'Unable to save changes.')
@el.find('.alert').removeClass('hidden').text(error_message)
)
App.Config.set('cdr_signal', { prio: 5100, name: 'Signal', parent: '#channels', target: '#channels/cdr_signal', controller: Index, permission: ['admin.channel_cdr_signal'] }, 'NavBarAdmin')

View file

@ -0,0 +1,249 @@
class Index extends App.ControllerSubContent
requiredPermission: 'admin.channel_cdr_voice'
events:
'click .js-new': 'new'
'click .js-edit': 'edit'
'click .js-delete': 'delete'
'click .js-disable': 'disable'
'click .js-enable': 'enable'
'click .js-rotate-token': 'rotateToken'
constructor: ->
super
#@interval(@load, 60000)
@load()
load: =>
@startLoading()
@ajax(
id: 'cdr_voice_index'
type: 'GET'
url: "#{@apiPath}/channels_cdr_voice"
processData: true
success: (data) =>
@stopLoading()
App.Collection.loadAssets(data.assets)
@render(data)
)
render: (data) =>
channels = []
for channel_id in data.channel_ids
channel = App.Channel.find(channel_id)
if channel && channel.options
displayName = '-'
if channel.group_id
group = App.Group.find(channel.group_id)
displayName = group.displayName()
channel.options.groupName = displayName
channels.push channel
@html App.view('cdr_voice/index')(
channels: channels
)
new: (e) =>
e.preventDefault()
new FormAdd(
container: @el.parents('.content')
load: @load
)
edit: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
channel = App.Channel.find(id)
new FormEdit(
channel: channel
container: @el.parents('.content')
load: @load
)
delete: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
new App.ControllerConfirm(
message: 'Sure?'
callback: =>
@ajax(
id: 'cdr_voice_delete'
type: 'DELETE'
url: "#{@apiPath}/channels_cdr_voice"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
container: @el.closest('.content')
)
rotateToken: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
new App.ControllerConfirm(
message: 'This will break the submission form!'
buttonSubmit: 'Reset token'
head: 'Reset the submission token?'
callback: =>
@ajax(
id: 'cdr_voice_disable'
type: 'POST'
url: "#{@apiPath}/channels_cdr_voice_rotate_token"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
container: @el.closest('.content')
)
disable: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
@ajax(
id: 'cdr_voice_disable'
type: 'POST'
url: "#{@apiPath}/channels_cdr_voice_disable"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
enable: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
@ajax(
id: 'cdr_voice_enable'
type: 'POST'
url: "#{@apiPath}/channels_cdr_voice_enable"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
class FormAdd extends App.ControllerModal
head: 'Add Web Form'
shown: true
button: 'Add'
buttonCancel: true
small: true
content: ->
content = $(App.view('cdr_voice/form_add')())
createOrgSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'organization_id'
multiple: false
limit: 100
null: false
relation: 'Organization'
nulloption: true
value: selected_id
class: 'form-control--small'
)
createGroupSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'group_id'
multiple: false
limit: 100
null: false
relation: 'Group'
nulloption: true
value: selected_id
class: 'form-control--small'
)
content.find('.js-select').on('click', (e) =>
@selectAll(e)
)
content.find('.js-messagesGroup').replaceWith createGroupSelection(1)
content.find('.js-organization').replaceWith createOrgSelection(null)
content
onClosed: =>
return if !@isChanged
@isChanged = false
@load()
onSubmit: (e) =>
@formDisable(e)
@ajax(
id: 'cdr_voice_app_verify'
type: 'POST'
url: "#{@apiPath}/channels_cdr_voice"
data: JSON.stringify(@formParams())
processData: true
success: =>
@isChanged = true
@close()
error: (xhr) =>
data = JSON.parse(xhr.responseText)
@formEnable(e)
error_message = App.i18n.translateContent(data.error || 'Unable to save Web Form.')
@el.find('.alert').removeClass('hidden').text(error_message)
)
class FormEdit extends App.ControllerModal
head: 'Web Form Info'
shown: true
buttonCancel: true
content: ->
content = $(App.view('cdr_voice/form_edit')(channel: @channel))
createOrgSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'organization_id'
multiple: false
limit: 100
null: false
relation: 'Organization'
nulloption: true
value: selected_id
class: 'form-control--small'
)
createGroupSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'group_id'
multiple: false
limit: 100
null: false
relation: 'Group'
nulloption: true
value: selected_id
class: 'form-control--small'
)
content.find('.js-messagesGroup').replaceWith createGroupSelection(@channel.group_id)
content.find('.js-organization').replaceWith createOrgSelection(@channel.options.organization_id)
content
onClosed: =>
return if !@isChanged
@isChanged = false
@load()
onSubmit: (e) =>
@formDisable(e)
params = @formParams()
@channel.options = params
@ajax(
id: 'channel_cdr_voice_update'
type: 'PUT'
url: "#{@apiPath}/channels_cdr_voice/#{@channel.id}"
data: JSON.stringify(@formParams())
processData: true
success: =>
@isChanged = true
@close()
error: (xhr) =>
data = JSON.parse(xhr.responseText)
@formEnable(e)
error_message = App.i18n.translateContent(data.error || 'Unable to save changes.')
@el.find('.alert').removeClass('hidden').text(error_message)
)
App.Config.set('cdr_voice', { prio: 5100, name: 'Voice', parent: '#channels', target: '#channels/cdr_voice', controller: Index, permission: ['admin.channel_cdr_voice'] }, 'NavBarAdmin')

View file

@ -0,0 +1,249 @@
class Index extends App.ControllerSubContent
requiredPermission: 'admin.channel_cdr_whatsapp'
events:
'click .js-new': 'new'
'click .js-edit': 'edit'
'click .js-delete': 'delete'
'click .js-disable': 'disable'
'click .js-enable': 'enable'
'click .js-rotate-token': 'rotateToken'
constructor: ->
super
#@interval(@load, 60000)
@load()
load: =>
@startLoading()
@ajax(
id: 'cdr_whatsapp_index'
type: 'GET'
url: "#{@apiPath}/channels_cdr_whatsapp"
processData: true
success: (data) =>
@stopLoading()
App.Collection.loadAssets(data.assets)
@render(data)
)
render: (data) =>
channels = []
for channel_id in data.channel_ids
channel = App.Channel.find(channel_id)
if channel && channel.options
displayName = '-'
if channel.group_id
group = App.Group.find(channel.group_id)
displayName = group.displayName()
channel.options.groupName = displayName
channels.push channel
@html App.view('cdr_whatsapp/index')(
channels: channels
)
new: (e) =>
e.preventDefault()
new FormAdd(
container: @el.parents('.content')
load: @load
)
edit: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
channel = App.Channel.find(id)
new FormEdit(
channel: channel
container: @el.parents('.content')
load: @load
)
delete: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
new App.ControllerConfirm(
message: 'Sure?'
callback: =>
@ajax(
id: 'cdr_whatsapp_delete'
type: 'DELETE'
url: "#{@apiPath}/channels_cdr_whatsapp"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
container: @el.closest('.content')
)
rotateToken: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
new App.ControllerConfirm(
message: 'This will break the submission form!'
buttonSubmit: 'Reset token'
head: 'Reset the submission token?'
callback: =>
@ajax(
id: 'cdr_whatsapp_disable'
type: 'POST'
url: "#{@apiPath}/channels_cdr_whatsapp_rotate_token"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
container: @el.closest('.content')
)
disable: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
@ajax(
id: 'cdr_whatsapp_disable'
type: 'POST'
url: "#{@apiPath}/channels_cdr_whatsapp_disable"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
enable: (e) =>
e.preventDefault()
id = $(e.target).closest('.action').data('id')
@ajax(
id: 'cdr_whatsapp_enable'
type: 'POST'
url: "#{@apiPath}/channels_cdr_whatsapp_enable"
data: JSON.stringify(id: id)
processData: true
success: =>
@load()
)
class FormAdd extends App.ControllerModal
head: 'Add Web Form'
shown: true
button: 'Add'
buttonCancel: true
small: true
content: ->
content = $(App.view('cdr_whatsapp/form_add')())
createOrgSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'organization_id'
multiple: false
limit: 100
null: false
relation: 'Organization'
nulloption: true
value: selected_id
class: 'form-control--small'
)
createGroupSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'group_id'
multiple: false
limit: 100
null: false
relation: 'Group'
nulloption: true
value: selected_id
class: 'form-control--small'
)
content.find('.js-select').on('click', (e) =>
@selectAll(e)
)
content.find('.js-messagesGroup').replaceWith createGroupSelection(1)
content.find('.js-organization').replaceWith createOrgSelection(null)
content
onClosed: =>
return if !@isChanged
@isChanged = false
@load()
onSubmit: (e) =>
@formDisable(e)
@ajax(
id: 'cdr_whatsapp_app_verify'
type: 'POST'
url: "#{@apiPath}/channels_cdr_whatsapp"
data: JSON.stringify(@formParams())
processData: true
success: =>
@isChanged = true
@close()
error: (xhr) =>
data = JSON.parse(xhr.responseText)
@formEnable(e)
error_message = App.i18n.translateContent(data.error || 'Unable to save Web Form.')
@el.find('.alert').removeClass('hidden').text(error_message)
)
class FormEdit extends App.ControllerModal
head: 'Web Form Info'
shown: true
buttonCancel: true
content: ->
content = $(App.view('cdr_whatsapp/form_edit')(channel: @channel))
createOrgSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'organization_id'
multiple: false
limit: 100
null: false
relation: 'Organization'
nulloption: true
value: selected_id
class: 'form-control--small'
)
createGroupSelection = (selected_id) ->
return App.UiElement.select.render(
name: 'group_id'
multiple: false
limit: 100
null: false
relation: 'Group'
nulloption: true
value: selected_id
class: 'form-control--small'
)
content.find('.js-messagesGroup').replaceWith createGroupSelection(@channel.group_id)
content.find('.js-organization').replaceWith createOrgSelection(@channel.options.organization_id)
content
onClosed: =>
return if !@isChanged
@isChanged = false
@load()
onSubmit: (e) =>
@formDisable(e)
params = @formParams()
@channel.options = params
@ajax(
id: 'channel_cdr_whatsapp_update'
type: 'PUT'
url: "#{@apiPath}/channels_cdr_whatsapp/#{@channel.id}"
data: JSON.stringify(@formParams())
processData: true
success: =>
@isChanged = true
@close()
error: (xhr) =>
data = JSON.parse(xhr.responseText)
@formEnable(e)
error_message = App.i18n.translateContent(data.error || 'Unable to save changes.')
@el.find('.alert').removeClass('hidden').text(error_message)
)
App.Config.set('cdr_whatsapp', { prio: 5100, name: 'Whatsapp', parent: '#channels', target: '#channels/cdr_whatsapp', controller: Index, permission: ['admin.channel_cdr_whatsapp'] }, 'NavBarAdmin')

View file

@ -0,0 +1,79 @@
class CdrSignalReply
@action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer')
if article.sender.name is 'Customer' && article.type.name is 'cdr_signal'
actions.push {
name: 'reply'
type: 'cdrSignalMessageReply'
icon: 'reply'
href: '#'
}
actions
@perform: (articleContainer, type, ticket, article, ui) ->
return true if type isnt 'cdrSignalMessageReply'
ui.scrollToCompose()
# get reference article
type = App.TicketArticleType.find(article.type_id)
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
if article.message_id
articleNew.in_reply_to = article.message_id
# get current body
articleNew.body = ui.el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || ''
App.Event.trigger('ui::ticket::setArticleType', {
ticket: ticket
type: type
article: articleNew
position: 'end'
})
true
@articleTypes: (articleTypes, ticket, ui) ->
return articleTypes if !ui.permissionCheck('ticket.agent')
return articleTypes if !ticket || !ticket.create_article_type_id
articleTypeCreate = App.TicketArticleType.find(ticket.create_article_type_id).name
return articleTypes if articleTypeCreate isnt 'cdr_signal'
articleTypes.push {
name: 'cdr_signal'
icon: 'cdr-signal'
attributes: []
internal: false,
features: ['attachment']
maxTextLength: 10000
warningTextLength: 5000
}
articleTypes
@setArticleTypePost: (type, ticket, ui) ->
return if type isnt 'cdr_signal'
rawHTML = ui.$('[data-name=body]').html()
cleanHTML = App.Utils.htmlRemoveRichtext(rawHTML)
if cleanHTML && cleanHTML.html() != rawHTML
ui.$('[data-name=body]').html(cleanHTML)
@params: (type, params, ui) ->
if type is 'cdr_signal'
App.Utils.htmlRemoveRichtext(ui.$('[data-name=body]'), false)
params.content_type = 'text/plain'
params.body = App.Utils.html2text(params.body, true)
params
App.Config.set('300-CdrSignalReply', CdrSignalReply, 'TicketZoomArticleAction')

View file

@ -0,0 +1,79 @@
class CdrWhatsappReply
@action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer')
if article.sender.name is 'Customer' && article.type.name is 'cdr_whatsapp'
actions.push {
name: 'reply'
type: 'cdrWhatsappMessageReply'
icon: 'reply'
href: '#'
}
actions
@perform: (articleContainer, type, ticket, article, ui) ->
return true if type isnt 'cdrWhatsappMessageReply'
ui.scrollToCompose()
# get reference article
type = App.TicketArticleType.find(article.type_id)
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
if article.message_id
articleNew.in_reply_to = article.message_id
# get current body
articleNew.body = ui.el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || ''
App.Event.trigger('ui::ticket::setArticleType', {
ticket: ticket
type: type
article: articleNew
position: 'end'
})
true
@articleTypes: (articleTypes, ticket, ui) ->
return articleTypes if !ui.permissionCheck('ticket.agent')
return articleTypes if !ticket || !ticket.create_article_type_id
articleTypeCreate = App.TicketArticleType.find(ticket.create_article_type_id).name
return articleTypes if articleTypeCreate isnt 'cdr_whatsapp'
articleTypes.push {
name: 'cdr_whatsapp'
icon: 'cdr-whatsapp'
attributes: []
internal: false,
features: ['attachment']
maxTextLength: 10000
warningTextLength: 5000
}
articleTypes
@setArticleTypePost: (type, ticket, ui) ->
return if type isnt 'cdr_whatsapp'
rawHTML = ui.$('[data-name=body]').html()
cleanHTML = App.Utils.htmlRemoveRichtext(rawHTML)
if cleanHTML && cleanHTML.html() != rawHTML
ui.$('[data-name=body]').html(cleanHTML)
@params: (type, params, ui) ->
if type is 'cdr_whatsapp'
App.Utils.htmlRemoveRichtext(ui.$('[data-name=body]'), false)
params.content_type = 'text/plain'
params.body = App.Utils.html2text(params.body, true)
params
App.Config.set('300-CdrWhatsappReply', CdrWhatsappReply, 'TicketZoomArticleAction')

View file

@ -0,0 +1,47 @@
<div class="alert alert--danger hidden" role="alert"></div>
<fieldset>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Phone number') %> <span>*</span></label>
</div>
<div class="controls">
<input id="phone_number" type="text" name="phone_number" value="" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Bot Token') %> <span>*</span></label>
</div>
<div class="controls">
<input id="bot_token" type="text" name="bot_token" value="" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Bot Endpoint') %> <span>*</span></label>
</div>
<div class="controls">
<input id="bot_endpoint" type="text" name="bot_endpoint" value="" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the group in which form submissions will get added to.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="js-messagesGroup"></div>
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the organization to which submitters will be added to when they submit via this form.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="profile-organization js-organization"></div>
</div>
</div>
</fieldset>

View file

@ -0,0 +1,55 @@
<div class="alert alert--danger hidden" role="alert"></div>
<fieldset>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Voice Line Number') %> <span>*</span></label>
</div>
<div class="controls">
<input id="phone_number" type="text" name="phone_number" value="<%= @channel.options.phone_number %>" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Bot Token') %> <span>*</span></label>
</div>
<div class="controls">
<input id="bot_token" type="text" name="bot_token" value="<%= @channel.options.bot_token %>" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Bot Endpoint') %> <span>*</span></label>
</div>
<div class="controls">
<input id="bot_endpoint" type="text" name="bot_endpoint" value="<%= @channel.options.bot_endpoint %>" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the group in which incoming messages will be added.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="js-messagesGroup"></div>
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the organization to which users will be added to when they send a message to this number.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="profile-organization js-organization"></div>
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="token"><%- @T('Endpoint URL') %> <span>*</span></label>
</div>
<div class="controls">
<input id="token" type="text" value="<%= "#{App.Config.get('http_type')}://#{App.Config.get('fqdn')}/api/v1/channels_cdr_signal_webhook/#{@channel.options.token}" %>" class="form-control input js-select" readonly>
</div>
</div>
</fieldset>

View file

@ -0,0 +1,49 @@
<div class="page-header">
<div class="page-header-title">
<h1><%- @T('Signal') %></h1>
</div>
<div class="page-header-meta">
<a class="btn btn--success js-new"><%- @T('Add Signal bot') %></a>
</div>
</div>
<div class="page-content">
<% if _.isEmpty(@channels): %>
<div class="page-description">
<p><%- @T('You have no configured %s right now.', 'Signal numbers') %></p>
</div>
<% else: %>
<% for channel in @channels: %>
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%= channel.id %>">
<div class="action-block action-row">
<h2><%- @Icon('status', 'supergood-color inline') %> <%= channel.options.phone_number %></h2>
</div>
<div class="action-flow action-flow--row">
<div class="action-block">
<h3><%- @T('Group') %></h3>
<% if channel.options: %>
<%= channel.options.groupName %>
<% end %>
</div>
<div class="action-block">
<h3><%- @T('Endpoint URL') %></h3>
<%- @T('Click the edit button to view the endpoint details ') %>
</div>
</div>
<div class="action-controls">
<div class="btn btn--danger btn--secondary js-delete"><%- @T('Delete') %></div>
<div class="btn btn--danger btn--secondary js-rotate-token"><%- @T('Reset Token') %></div>
<% if channel.active is true: %>
<div class="btn btn--secondary js-disable"><%- @T('Disable') %></div>
<% else: %>
<div class="btn btn--secondary js-enable"><%- @T('Enable') %></div>
<% end %>
<div class="btn js-edit"><%- @T('Edit') %></div>
</div>
</div>
<% end %>
</div>

View file

@ -0,0 +1,29 @@
<div class="alert alert--danger hidden" role="alert"></div>
<fieldset>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Phone number') %> <span>*</span></label>
</div>
<div class="controls">
<input id="phone_number" type="text" name="phone_number" value="" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the group in which form submissions will get added to.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="js-messagesGroup"></div>
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the organization to which submitters will be added to when they submit via this form.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="profile-organization js-organization"></div>
</div>
</div>
</fieldset>

View file

@ -0,0 +1,37 @@
<div class="alert alert--danger hidden" role="alert"></div>
<fieldset>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Voice Line Number') %> <span>*</span></label>
</div>
<div class="controls">
<input id="phone_number" type="text" name="phone_number" value="<%= @channel.options.phone_number %>" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the group in which incoming calls will be added to.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="js-messagesGroup"></div>
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the organization to which users will be added to when they leave a recording to this voice line.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="profile-organization js-organization"></div>
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="token"><%- @T('Endpoint URL') %> <span>*</span></label>
</div>
<div class="controls">
<input id="token" type="text" value="<%= "#{App.Config.get('http_type')}://#{App.Config.get('fqdn')}/api/v1/channels_cdr_voice_webhook/#{@channel.options.token}" %>" class="form-control input js-select" readonly>
</div>
</div>
</fieldset>

View file

@ -0,0 +1,49 @@
<div class="page-header">
<div class="page-header-title">
<h1><%- @T('Voice') %></h1>
</div>
<div class="page-header-meta">
<a class="btn btn--success js-new"><%- @T('Add Voice Line') %></a>
</div>
</div>
<div class="page-content">
<% if _.isEmpty(@channels): %>
<div class="page-description">
<p><%- @T('You have no configured %s right now.', 'voice lines') %></p>
</div>
<% else: %>
<% for channel in @channels: %>
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%= channel.id %>">
<div class="action-block action-row">
<h2><%- @Icon('status', 'supergood-color inline') %> <%= channel.options.phone_number %></h2>
</div>
<div class="action-flow action-flow--row">
<div class="action-block">
<h3><%- @T('Group') %></h3>
<% if channel.options: %>
<%= channel.options.groupName %>
<% end %>
</div>
<div class="action-block">
<h3><%- @T('Endpoint URL') %></h3>
<%- @T('Click the edit button to view the endpoint details ') %>
</div>
</div>
<div class="action-controls">
<div class="btn btn--danger btn--secondary js-delete"><%- @T('Delete') %></div>
<div class="btn btn--danger btn--secondary js-rotate-token"><%- @T('Reset Token') %></div>
<% if channel.active is true: %>
<div class="btn btn--secondary js-disable"><%- @T('Disable') %></div>
<% else: %>
<div class="btn btn--secondary js-enable"><%- @T('Enable') %></div>
<% end %>
<div class="btn js-edit"><%- @T('Edit') %></div>
</div>
</div>
<% end %>
</div>

View file

@ -0,0 +1,47 @@
<div class="alert alert--danger hidden" role="alert"></div>
<fieldset>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Phone number') %> <span>*</span></label>
</div>
<div class="controls">
<input id="phone_number" type="text" name="phone_number" value="" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Bot Token') %> <span>*</span></label>
</div>
<div class="controls">
<input id="bot_token" type="text" name="bot_token" value="" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Bot Endpoint') %> <span>*</span></label>
</div>
<div class="controls">
<input id="bot_endpoint" type="text" name="bot_endpoint" value="" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the group in which form submissions will get added to.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="js-messagesGroup"></div>
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the organization to which submitters will be added to when they submit via this form.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="profile-organization js-organization"></div>
</div>
</div>
</fieldset>

View file

@ -0,0 +1,55 @@
<div class="alert alert--danger hidden" role="alert"></div>
<fieldset>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Whatsapp Number') %> <span>*</span></label>
</div>
<div class="controls">
<input id="phone_number" type="text" name="phone_number" value="<%= @channel.options.phone_number %>" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Bot Token') %> <span>*</span></label>
</div>
<div class="controls">
<input id="bot_token" type="text" name="bot_token" value="<%= @channel.options.bot_token %>" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="form_name"><%- @T('Bot Endpoint') %> <span>*</span></label>
</div>
<div class="controls">
<input id="bot_endpoint" type="text" name="bot_endpoint" value="<%= @channel.options.bot_endpoint %>" class="form-control" required autocomplete="off">
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the group in which incoming messages will be added to.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="js-messagesGroup"></div>
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Choose the organization to which users will be added to when they send a message to this number.') %> <span>*</span></label>
</div>
<div class="controls">
<div class="profile-organization js-organization"></div>
</div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for="token"><%- @T('Endpoint URL') %> <span>*</span></label>
</div>
<div class="controls">
<input id="token" type="text" value="<%= "#{App.Config.get('http_type')}://#{App.Config.get('fqdn')}/api/v1/channels_cdr_whatsapp_webhook/#{@channel.options.token}" %>" class="form-control input js-select" readonly>
</div>
</div>
</fieldset>

View file

@ -0,0 +1,49 @@
<div class="page-header">
<div class="page-header-title">
<h1><%- @T('Whatsapp') %></h1>
</div>
<div class="page-header-meta">
<a class="btn btn--success js-new"><%- @T('Add Whatsapp bot') %></a>
</div>
</div>
<div class="page-content">
<% if _.isEmpty(@channels): %>
<div class="page-description">
<p><%- @T('You have no configured %s right now.', 'Whatsapp numbers') %></p>
</div>
<% else: %>
<% for channel in @channels: %>
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%= channel.id %>">
<div class="action-block action-row">
<h2><%- @Icon('status', 'supergood-color inline') %> <%= channel.options.phone_number %></h2>
</div>
<div class="action-flow action-flow--row">
<div class="action-block">
<h3><%- @T('Group') %></h3>
<% if channel.options: %>
<%= channel.options.groupName %>
<% end %>
</div>
<div class="action-block">
<h3><%- @T('Endpoint URL') %></h3>
<%- @T('Click the edit button to view the endpoint details ') %>
</div>
</div>
<div class="action-controls">
<div class="btn btn--danger btn--secondary js-delete"><%- @T('Delete') %></div>
<div class="btn btn--danger btn--secondary js-rotate-token"><%- @T('Reset Token') %></div>
<% if channel.active is true: %>
<div class="btn btn--secondary js-disable"><%- @T('Disable') %></div>
<% else: %>
<div class="btn btn--secondary js-enable"><%- @T('Enable') %></div>
<% end %>
<div class="btn js-edit"><%- @T('Edit') %></div>
</div>
</div>
<% end %>
</div>

View file

@ -0,0 +1,4 @@
.icon-cdr-signal {
width: 17px;
height: 17px;
}

View file

@ -0,0 +1,4 @@
.icon-cdr-whatsapp {
width: 17px;
height: 17px;
}

View file

@ -0,0 +1,268 @@
# frozen_string_literal: true
class ChannelsCdrSignalController < ApplicationController
prepend_before_action -> { authentication_check && authorize! }, except: [:webhook]
skip_before_action :verify_csrf_token, only: [:webhook]
include CreatesTicketArticles
def index
assets = {}
channel_ids = []
Channel.where(area: 'Signal::Number').order(:id).each do |channel|
assets = channel.assets(assets)
channel_ids.push channel.id
end
render json: {
assets: assets,
channel_ids: channel_ids
}
end
def add
begin
errors = {}
errors['group_id'] = 'required' unless params[:group_id].present?
if errors.present?
render json: {
errors: errors
}, status: :bad_request
return
end
channel = Channel.create(
area: 'Signal::Number',
options: {
adapter: 'cdr_signal',
phone_number: params[:phone_number],
bot_token: params[:bot_token],
bot_endpoint: params[:bot_endpoint],
token: SecureRandom.urlsafe_base64(48),
organization_id: params[:organization_id]
},
group_id: params[:group_id],
active: true
)
rescue StandardError => e
raise Exceptions::UnprocessableEntity, e.message
end
render json: channel
end
def update
errors = {}
errors['group_id'] = 'required' unless params[:group_id].present?
if errors.present?
render json: {
errors: errors
}, status: :bad_request
return
end
channel = Channel.find_by(id: params[:id], area: 'Signal::Number')
begin
channel.options[:phone_number] = params[:phone_number]
channel.options[:bot_token] = params[:bot_token]
channel.options[:bot_endpoint] = params[:bot_endpoint]
channel.options[:organization_id] = params[:organization_id]
channel.group_id = params[:group_id]
channel.save!
rescue StandardError => e
raise Exceptions::UnprocessableEntity, e.message
end
render json: channel
end
def rotate_token
channel = Channel.find_by(id: params[:id], area: 'Signal::Number')
channel.options[:token] = SecureRandom.urlsafe_base64(48)
channel.save!
render json: {}
end
def enable
channel = Channel.find_by(id: params[:id], area: 'Signal::Number')
channel.active = true
channel.save!
render json: {}
end
def disable
channel = Channel.find_by(id: params[:id], area: 'Signal::Number')
channel.active = false
channel.save!
render json: {}
end
def destroy
channel = Channel.find_by(id: params[:id], area: 'Signal::Number')
channel.destroy
render json: {}
end
def channel_for_token(token)
return false unless token
Channel.where(area: 'Signal::Number').each do |channel|
return channel if channel.options[:token] == token
end
false
end
def webhook
token = params['token']
return render json: {}, status: 401 unless token
channel = channel_for_token(token)
return render json: {}, status: 401 if !channel || !channel.active
return render json: {}, status: 401 if channel.options[:token] != token
channel_id = channel.id
# validate input
errors = {}
# %i[to
# from
# message_id
# sent_at].each | field |
# (errors[field] = 'required' if params[field].blank?)
if errors.present?
render json: {
errors: errors
}, status: :bad_request
return
end
message_id = params[:message_id]
return if Ticket::Article.exists?(message_id: "cdr_signal.#{message_id}")
receiver_phone_number = params[:to].strip
sender_phone_number = params[:from].strip
customer = User.find_by(phone: sender_phone_number)
customer ||= User.find_by(mobile: sender_phone_number)
unless customer
role_ids = Role.signup_role_ids
customer = User.create(
firstname: '',
lastname: '',
email: '',
password: '',
phone: sender_phone_number,
note: 'CDR Signal',
active: true,
role_ids: role_ids,
updated_by_id: 1,
created_by_id: 1
)
end
# set current user
UserInfo.current_user_id = customer.id
current_user_set(customer, 'token_auth')
group = Group.find_by(id: channel.group_id)
if group.blank?
Rails.logger.error "Signal channel #{channel_id} paired with Group #{channel.group_id}, but group does not exist!"
return render json: { error: 'There was an error during Signal submission' }, status: 500
end
organization_id = channel.options['organization_id']
if organization_id.present?
organization = Organization.find_by(id: organization_id)
unless organization.present?
Rails.logger.error "Signal channel #{channel_id} paired with Organization #{organization_id}, but organization does not exist!"
return render json: { error: 'There was an error during Signal submission' }, status: 500
end
unless customer.organization_id.present?
customer.organization_id = organization.id
customer.save!
end
end
message = params[:message] ||= 'No text content'
sent_at = params[:sent_at]
attachment_data_base64 = params[:attachment]
attachment_filename = params[:filename]
attachment_mimetype = params[:mime_type]
title = "Message from #{sender_phone_number} at #{sent_at}"
body = message
# find ticket or create one
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
ticket = Ticket.where(customer_id: customer.id).where.not(state_id: state_ids).order(:updated_at).first
if ticket
# check if title need to be updated
ticket.title = title if ticket.title == '-'
new_state = Ticket::State.find_by(default_create: true)
ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id
else
ticket = Ticket.new(
group_id: channel.group_id,
title: title,
customer_id: customer.id,
preferences: {
channel_id: channel.id,
cdr_signal: {
bot_token: channel.options[:bot_token], # change to bot id
chat_id: sender_phone_number
}
}
)
end
ticket.save!
article_params = {
from: sender_phone_number,
to: receiver_phone_number,
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
subject: title,
body: body,
content_type: 'text/plain',
message_id: "cdr_signal.#{message_id}",
ticket_id: ticket.id,
internal: false,
preferences: {
cdr_signal: {
timestamp: sent_at,
message_id: message_id,
from: sender_phone_number
}
}
}
if attachment_data_base64.present?
article_params[:attachments] = [
# i don't even...
# this is necessary because of what's going on in controllers/concerns/creates_ticket_articles.rb
# we need help from the ruby gods
{
'filename' => attachment_filename,
:filename => attachment_filename,
:data => attachment_data_base64,
'data' => attachment_data_base64,
'mime-type' => attachment_mimetype
}
]
end
ticket.with_lock do
ta = article_create(ticket, article_params)
ta.update!(type_id: Ticket::Article::Type.find_by(name: 'cdr_signal').id)
end
ticket.update!(create_article_type_id: Ticket::Article::Type.find_by(name: 'cdr_signal').id)
result = {
ticket: {
id: ticket.id,
number: ticket.number
}
}
render json: result, status: :ok
end
end

View file

@ -0,0 +1,253 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
class ChannelsCdrVoiceController < ApplicationController
prepend_before_action -> { authentication_check && authorize! }, except: [:webhook]
skip_before_action :verify_csrf_token, only: [:webhook]
include CreatesTicketArticles
def index
assets = {}
channel_ids = []
Channel.where(area: 'Voice::Number').order(:id).each do |channel|
assets = channel.assets(assets)
channel_ids.push channel.id
end
render json: {
assets: assets,
channel_ids: channel_ids
}
end
def add
begin
errors = {}
errors['group_id'] = 'required' unless params[:group_id].present?
if errors.present?
render json: {
errors: errors
}, status: :bad_request
return
end
channel = Channel.create(
area: 'Voice::Number',
options: {
phone_number: params[:phone_number],
token: SecureRandom.urlsafe_base64(48),
organization_id: params[:organization_id]
},
group_id: params[:group_id],
active: true
)
rescue StandardError => e
raise Exceptions::UnprocessableEntity, e.message
end
render json: channel
end
def update
errors = {}
errors['group_id'] = 'required' unless params[:group_id].present?
if errors.present?
render json: {
errors: errors
}, status: :bad_request
return
end
channel = Channel.find_by(id: params[:id], area: 'Voice::Number')
begin
channel.options[:phone_number] = params[:phone_number]
channel.options[:organization_id] = params[:organization_id]
channel.group_id = params[:group_id]
channel.save!
rescue StandardError => e
raise Exceptions::UnprocessableEntity, e.message
end
render json: channel
end
def rotate_token
channel = Channel.find_by(id: params[:id], area: 'Voice::Number')
channel.options[:token] = SecureRandom.urlsafe_base64(48)
channel.save!
render json: {}
end
def enable
channel = Channel.find_by(id: params[:id], area: 'Voice::Number')
channel.active = true
channel.save!
render json: {}
end
def disable
channel = Channel.find_by(id: params[:id], area: 'Voice::Number')
channel.active = false
channel.save!
render json: {}
end
def destroy
channel = Channel.find_by(id: params[:id], area: 'Voice::Number')
channel.destroy
render json: {}
end
def channel_for_token(token)
return false unless token
Channel.where(area: 'Voice::Number').each do |channel|
return channel if channel.options[:token] == token
end
false
end
def webhook
token = params['token']
return render json: {}, status: 401 unless token
channel = channel_for_token(token)
return render json: {}, status: 401 if !channel || !channel.active
return render json: {}, status: 401 if channel.options[:token] != token
channel_id = channel.id
# validate input
errors = {}
%i[to
from
duration
startTime
endTime
recording
mimeType
callSid].each do |field|
errors[field] = 'required' if params[field].blank?
end
valid_mimetypes = ['audio/mpeg']
unless valid_mimetypes.include?(params[:mimeType])
errors[:mimeType] = "invalid. must be one of #{valid_mimetypes.join(',')}"
end
receiver_phone_number = params[:to]
if errors.present?
render json: {
errors: errors
}, status: :bad_request
return
end
caller_phone_number = params[:from].strip
customer = User.find_by(phone: caller_phone_number)
customer ||= User.find_by(mobile: caller_phone_number)
unless customer
role_ids = Role.signup_role_ids
customer = User.create(
firstname: '',
lastname: '',
email: '',
password: '',
phone: caller_phone_number,
active: true,
role_ids: role_ids,
updated_by_id: 1,
created_by_id: 1
)
end
# set current user
UserInfo.current_user_id = customer.id
current_user_set(customer, 'token_auth')
group = Group.find_by(id: channel.group_id)
unless group.present?
Rails.logger.error "Voice channel #{channel_id} paired with Group #{channel.group_id}, but group does not exist!"
return render json: { error: 'There was an error during voice submission' }, status: 500
end
organization_id = channel.options['organization_id']
if organization_id.present?
organization = Organization.find_by(id: organization_id)
unless organization.present?
Rails.logger.error "Voice channel #{channel_id} paired with Organization #{organization_id}, but organization does not exist!"
return render json: { error: 'There was an error during voice submission' }, status: 500
end
unless customer.organization_id.present?
customer.organization_id = organization.id
customer.save!
end
end
call_id = params[:calLSid]
duration = params[:duration]
start_time = params[:startTime]
end_time = params[:endTime]
recording_data_base64 = params[:recording]
recording_filename = "phone-call-#{start_time}-#{call_id}.mp3"
recording_mimetype = params[:mimeType]
title = "Call from #{caller_phone_number} at #{start_time}"
body = %(
<ul>
<li>Caller: #{caller_phone_number}</li>
<li>Service Number: #{receiver_phone_number}</li>
<li>Call Duration: #{duration} seconds</li>
<li>Start Time: #{start_time}</li>
<li>End Time: #{end_time}</li>
</ul>
<p>See the attached recording.</p>
)
ticket_params = {
group_id: group.id,
customer_id: customer.id,
title: title,
preferences: {},
note: 'This ticket was created from a recorded voice message.'
}
article_params = {
sender: 'Customer',
subject: title,
body: body,
content_type: 'text/html',
type: 'note',
attachments: [
# i don't even...
# this is necessary because of what's going on in controllers/concerns/creates_ticket_articles.rb
# we need help from the ruby gods
{
'filename' => recording_filename,
:filename => recording_filename,
:data => recording_data_base64,
'data' => recording_data_base64,
'mime-type' => recording_mimetype
}
]
}
clean_params = Ticket.param_cleanup(ticket_params, true)
ticket = Ticket.new(clean_params)
ticket.save!
ticket.with_lock do
article_params[:sender] = 'Customer'
article_create(ticket, article_params)
end
result = {
ticket: {
id: ticket.id,
number: ticket.number
}
}
render json: result, status: :ok
end
end

View file

@ -0,0 +1,270 @@
# frozen_string_literal: true
class ChannelsCdrWhatsappController < ApplicationController
prepend_before_action -> { authentication_check && authorize! }, except: [:webhook]
skip_before_action :verify_csrf_token, only: [:webhook]
include CreatesTicketArticles
def index
assets = {}
channel_ids = []
Channel.where(area: 'Whatsapp::Number').order(:id).each do |channel|
assets = channel.assets(assets)
channel_ids.push channel.id
end
render json: {
assets: assets,
channel_ids: channel_ids
}
end
def add
begin
errors = {}
errors['group_id'] = 'required' if params[:group_id].blank?
if errors.present?
render json: {
errors: errors
}, status: :bad_request
return
end
channel = Channel.create(
area: 'Whatsapp::Number',
options: {
adapter: 'cdr_whatsapp',
phone_number: params[:phone_number],
bot_token: params[:bot_token],
bot_endpoint: params[:bot_endpoint],
token: SecureRandom.urlsafe_base64(48),
organization_id: params[:organization_id]
},
group_id: params[:group_id],
active: true
)
rescue StandardError => e
raise Exceptions::UnprocessableEntity, e.message
end
render json: channel
end
def update
errors = {}
errors['group_id'] = 'required' if params[:group_id].blank?
if errors.present?
render json: {
errors: errors
}, status: :bad_request
return
end
channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number')
begin
channel.options[:phone_number] = params[:phone_number]
channel.options[:bot_token] = params[:bot_token]
channel.options[:bot_endpoint] = params[:bot_endpoint]
channel.options[:organization_id] = params[:organization_id]
channel.group_id = params[:group_id]
channel.save!
rescue StandardError => e
raise Exceptions::UnprocessableEntity, e.message
end
render json: channel
end
def rotate_token
channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number')
channel.options[:token] = SecureRandom.urlsafe_base64(48)
channel.save!
render json: {}
end
def enable
channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number')
channel.active = true
channel.save!
render json: {}
end
def disable
channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number')
channel.active = false
channel.save!
render json: {}
end
def destroy
channel = Channel.find_by(id: params[:id], area: 'Whatsapp::Number')
channel.destroy
render json: {}
end
def channel_for_token(token)
return false unless token
Channel.where(area: 'Whatsapp::Number').each do |channel|
return channel if channel.options[:token] == token
end
false
end
def webhook
token = params['token']
return render json: {}, status: :unauthorized unless token
channel = channel_for_token(token)
return render json: {}, status: :unauthorized if !channel || !channel.active
return render json: {}, status: :unauthorized if channel.options[:token] != token
channel_id = channel.id
# validate input
errors = {}
%i[to
from
message_id
sent_at].each do |field|
errors[field] = 'required' if params[field].blank?
end
if errors.present?
render json: {
errors: errors
}, status: :bad_request
return
end
message_id = params[:message_id]
return if Ticket::Article.exists?(message_id: "cdr_whatsapp.#{message_id}")
receiver_phone_number = params[:to].strip
sender_phone_number = params[:from].strip
customer = User.find_by(phone: sender_phone_number)
customer ||= User.find_by(mobile: sender_phone_number)
unless customer
role_ids = Role.signup_role_ids
customer = User.create(
firstname: '',
lastname: '',
email: '',
password: '',
phone: sender_phone_number,
note: 'CDR Whatsapp',
active: true,
role_ids: role_ids,
updated_by_id: 1,
created_by_id: 1
)
end
# set current user
UserInfo.current_user_id = customer.id
current_user_set(customer, 'token_auth')
group = Group.find_by(id: channel.group_id)
if group.blank?
Rails.logger.error "Whatsapp channel #{channel_id} paired with Group #{channel.group_id}, but group does not exist!"
return render json: { error: 'There was an error during Whatsapp submission' }, status: :internal_server_error
end
organization_id = channel.options['organization_id']
if organization_id.present?
organization = Organization.find_by(id: organization_id)
if organization.blank?
Rails.logger.error "Whatsapp channel #{channel_id} paired with Organization #{organization_id}, but organization does not exist!"
return render json: { error: 'There was an error during Whatsapp submission' }, status: :internal_server_error
end
if customer.organization_id.blank?
customer.organization_id = organization.id
customer.save!
end
end
message = params[:message] ||= 'No text content'
sent_at = params[:sent_at]
attachment_data_base64 = params[:attachment]
attachment_filename = params[:filename]
attachment_mimetype = params[:mime_type]
title = "Message from #{sender_phone_number} at #{sent_at}"
body = message
# find ticket or create one
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
ticket = Ticket.where(customer_id: customer.id).where.not(state_id: state_ids).order(:updated_at).first
if ticket
# check if title need to be updated
ticket.title = title if ticket.title == '-'
new_state = Ticket::State.find_by(default_create: true)
ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id
else
ticket = Ticket.new(
group_id: channel.group_id,
title: title,
customer_id: customer.id,
preferences: {
channel_id: channel.id,
cdr_whatsapp: {
bot_token: channel.options[:bot_token], # change to bot id
chat_id: sender_phone_number
}
}
)
end
ticket.save!
article_params = {
from: sender_phone_number,
to: receiver_phone_number,
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
subject: title,
body: body,
content_type: 'text/plain',
message_id: "cdr_whatsapp.#{message_id}",
ticket_id: ticket.id,
internal: false,
preferences: {
cdr_whatsapp: {
timestamp: sent_at,
message_id: message_id,
from: sender_phone_number
}
}
}
if attachment_data_base64.present?
article_params[:attachments] = [
# i don't even...
# this is necessary because of what's going on in controllers/concerns/creates_ticket_articles.rb
# we need help from the ruby gods
{
'filename' => attachment_filename,
:filename => attachment_filename,
:data => attachment_data_base64,
'data' => attachment_data_base64,
'mime-type' => attachment_mimetype
}
]
end
# setting the article type after saving seems to be the only way to get it to stick
ticket.with_lock do
ta = article_create(ticket, article_params)
ta.update!(type_id: Ticket::Article::Type.find_by(name: 'cdr_whatsapp').id)
end
ticket.update!(create_article_type_id: Ticket::Article::Type.find_by(name: 'cdr_whatsapp').id)
result = {
ticket: {
id: ticket.id,
number: ticket.number
}
}
render json: result, status: :ok
end
end

View file

@ -0,0 +1,55 @@
# frozen_string_literal: true
class Channel
class Driver
class CdrSignal
def fetchable?(_channel)
false
end
def disconnect; end
#
# instance = Channel::Driver::CdrSignal.new
# instance.send(
# {
# adapter: 'cdrsignal',
# auth: {
# api_key: api_key
# },
# },
# signal_attributes,
# notification
# )
#
def send(options, article, _notification = false)
# return if we run import mode
return if Setting.get('import_mode')
options = check_external_credential(options)
Rails.logger.debug { 'signal send started' }
Rails.logger.debug { options.inspect }
@signal = ::CdrSignal.new(options[:bot_endpoint], options[:bot_token])
@signal.from_article(article)
end
def self.streamable?
false
end
private
def check_external_credential(options)
if options[:auth] && options[:auth][:external_credential_id]
external_credential = ExternalCredential.find_by(id: options[:auth][:external_credential_id])
raise "No such ExternalCredential.find(#{options[:auth][:external_credential_id]})" unless external_credential
options[:auth][:api_key] = external_credential.credentials['api_key']
end
options
end
end
end
end

View file

@ -0,0 +1,55 @@
# frozen_string_literal: true
class Channel
class Driver
class CdrWhatsapp
def fetchable?(_channel)
false
end
def disconnect; end
#
# instance = Channel::Driver::CdrWhatsapp.new
# instance.send(
# {
# adapter: 'cdr_whatsapp',
# auth: {
# api_key: api_key
# },
# },
# whatsapp_attributes,
# notification
# )
#
def send(options, article, _notification = false)
# return if we run import mode
return if Setting.get('import_mode')
options = check_external_credential(options)
Rails.logger.debug { 'whatsapp send started' }
Rails.logger.debug { options.inspect }
@whatsapp = ::CdrWhatsapp.new(options[:bot_endpoint], options[:bot_token])
@whatsapp.from_article(article)
end
def self.streamable?
false
end
private
def check_external_credential(options)
if options[:auth] && options[:auth][:external_credential_id]
external_credential = ExternalCredential.find_by(id: options[:auth][:external_credential_id])
raise "No such ExternalCredential.find(#{options[:auth][:external_credential_id]})" unless external_credential
options[:auth][:api_key] = external_credential.credentials['api_key']
end
options
end
end
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
class Observer::Ticket::Article::CommunicateCdrSignal < ActiveRecord::Observer
observe 'ticket::_article'
def after_create(record)
# return if we run import mode
return true if Setting.get('import_mode')
# if sender is customer, do not communicate
return true unless record.sender_id
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
return true if sender.nil?
return true if sender.name == 'Customer'
# only apply on signal messages
return true unless record.type_id
type = Ticket::Article::Type.lookup(id: record.type_id)
return true if type.name !~ /\Acdr_signal/i
Delayed::Job.enqueue(Observer::Ticket::Article::CommunicateCdrSignal::BackgroundJob.new(record.id))
end
end

View file

@ -0,0 +1,137 @@
# frozen_string_literal: true
module Observer
module Ticket
module Article
class CommunicateCdrSignal
class BackgroundJob
def initialize(id)
@article_id = id
end
def perform
article = ::Ticket::Article.find(@article_id)
# set retry count
article.preferences['delivery_retry'] ||= 0
article.preferences['delivery_retry'] += 1
ticket = ::Ticket.lookup(id: article.ticket_id)
Rails.logger.debug { 'Signal background job' }
Rails.logger.debug { ticket.inspect }
Rails.logger.debug { article.inspect }
unless ticket.preferences
log_error(article,
"Can't find ticket.preferences for Ticket.find(#{article.ticket_id})")
end
unless ticket.preferences['cdr_signal']
log_error(article,
"Can't find ticket.preferences['cdr_signal'] for Ticket.find(#{article.ticket_id})")
end
unless ticket.preferences['cdr_signal']['chat_id']
log_error(article,
"Can't find ticket.preferences['cdr_signal']['chat_id'] for Ticket.find(#{article.ticket_id})")
end
unless ticket.preferences['cdr_signal']['bot_token']
log_error(article,
"Can't find ticket.preferences['cdr_signal']['bot_token'] for Ticket.find(#{article.ticket_id})")
end
channel = ::CdrSignal.bot_by_bot_token(ticket.preferences['cdr_signal']['bot_token'])
Rails.logger.debug { "signal got channel for #{channel.inspect}" }
channel ||= ::Channel.lookup(id: ticket.preferences['channel_id'])
unless channel
log_error(article,
"No such channel for bot #{ticket.preferences['cdr_signal']['bot_token']} or channel id #{ticket.preferences['channel_id']}")
end
if channel.options[:bot_token].blank?
log_error(article,
"Channel.find(#{channel.id}) has no signal api token!")
end
begin
result = channel.deliver(
to: ticket.preferences[:cdr_signal][:chat_id],
body: article.body
)
rescue StandardError => e
log_error(article, e.message)
return
end
Rails.logger.debug { "send result: #{result}" }
if result.nil? || result[:error].present?
log_error(article, 'Delivering signal message failed!')
return
end
article.to = result['result']['recipient']
article.from = result['result']['source']
message_id = format('%<source>s@%<timestamp>s', source: result['result']['source'],
timestamp: result['result']['timestamp'])
article.preferences['cdr_signal'] = {
timestamp: result['result']['timestamp'],
message_id: message_id,
from: result['result']['source'],
to: result['result']['recipient']
}
# set delivery status
article.preferences['delivery_status_message'] = nil
article.preferences['delivery_status'] = 'success'
article.preferences['delivery_status_date'] = Time.zone.now
article.message_id = "cdr_signal.#{message_id}"
article.save!
Rails.logger.info "Sent signal message to: '#{article.to}' (from #{article.from})"
article
end
def log_error(local_record, message)
local_record.preferences['delivery_status'] = 'fail'
local_record.preferences['delivery_status_message'] =
message.encode('UTF-8', 'UTF-8', invalid: :replace, replace: '?')
local_record.preferences['delivery_status_date'] = Time.zone.now
local_record.save
Rails.logger.error message
if local_record.preferences['delivery_retry'] > 3
::Ticket::Article.create(
ticket_id: local_record.ticket_id,
content_type: 'text/plain',
body: "Unable to send signal message: #{message}",
internal: true,
sender: ::Ticket::Article::Sender.find_by(name: 'System'),
type: ::Ticket::Article::Type.find_by(name: 'note'),
preferences: {
delivery_article_id_related: local_record.id,
delivery_message: true
},
updated_by_id: 1,
created_by_id: 1
)
end
raise message
end
def max_attempts
4
end
def reschedule_at(current_time, attempts)
return current_time + attempts * 120.seconds if Rails.env.production?
current_time + 5.seconds
end
end
end
end
end
end

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
class Observer::Ticket::Article::CommunicateCdrWhatsapp < ActiveRecord::Observer
observe 'ticket::_article'
def after_create(record)
# return if we run import mode
return true if Setting.get('import_mode')
# if sender is customer, do not communicate
return true unless record.sender_id
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
return true if sender.nil?
return true if sender.name == 'Customer'
# only apply on whatsapp messages
return true unless record.type_id
type = Ticket::Article::Type.lookup(id: record.type_id)
return true if type.name !~ /\Acdr_whatsapp/i
Delayed::Job.enqueue(Observer::Ticket::Article::CommunicateCdrWhatsapp::BackgroundJob.new(record.id))
end
end

View file

@ -0,0 +1,137 @@
# frozen_string_literal: true
module Observer
module Ticket
module Article
class CommunicateCdrWhatsapp
class BackgroundJob
def initialize(id)
@article_id = id
end
def perform
article = ::Ticket::Article.find(@article_id)
# set retry count
article.preferences['delivery_retry'] ||= 0
article.preferences['delivery_retry'] += 1
ticket = ::Ticket.lookup(id: article.ticket_id)
Rails.logger.debug { 'Whatsapp background job' }
Rails.logger.debug { ticket.inspect }
Rails.logger.debug { article.inspect }
unless ticket.preferences
log_error(article,
"Can't find ticket.preferences for Ticket.find(#{article.ticket_id})")
end
unless ticket.preferences['cdr_whatsapp']
log_error(article,
"Can't find ticket.preferences['cdr_whatsapp'] for Ticket.find(#{article.ticket_id})")
end
unless ticket.preferences['cdr_whatsapp']['chat_id']
log_error(article,
"Can't find ticket.preferences['cdr_whatsapp']['chat_id'] for Ticket.find(#{article.ticket_id})")
end
unless ticket.preferences['cdr_whatsapp']['bot_token']
log_error(article,
"Can't find ticket.preferences['cdr_whatsapp']['bot_token'] for Ticket.find(#{article.ticket_id})")
end
channel = ::CdrWhatsapp.bot_by_bot_token(ticket.preferences['cdr_whatsapp']['bot_token'])
Rails.logger.debug { "whatsapp got channel for #{channel.inspect}" }
channel ||= ::Channel.lookup(id: ticket.preferences['channel_id'])
unless channel
log_error(article,
"No such channel for bot #{ticket.preferences['cdr_whatsapp']['bot_token']} or channel id #{ticket.preferences['channel_id']}")
end
if channel.options[:bot_token].blank?
log_error(article,
"Channel.find(#{channel.id}) has no whatsapp api token!")
end
begin
result = channel.deliver(
to: ticket.preferences[:cdr_whatsapp][:chat_id],
body: article.body
)
rescue StandardError => e
log_error(article, e.message)
return
end
Rails.logger.debug { "send result: #{result}" }
if result.nil? || result[:error].present?
log_error(article, 'Delivering whatsapp message failed!')
return
end
article.to = result['result']['recipient']
article.from = result['result']['source']
message_id = format('%<source>s@%<timestamp>s', source: result['result']['source'],
timestamp: result['result']['timestamp'])
article.preferences['cdr_whatsapp'] = {
timestamp: result['result']['timestamp'],
message_id: message_id,
from: result['result']['source'],
to: result['result']['recipient']
}
# set delivery status
article.preferences['delivery_status_message'] = nil
article.preferences['delivery_status'] = 'success'
article.preferences['delivery_status_date'] = Time.zone.now
article.message_id = "cdr_whatsapp.#{message_id}"
article.save!
Rails.logger.info "Sent whatsapp message to: '#{article.to}' (from #{article.from})"
article
end
def log_error(local_record, message)
local_record.preferences['delivery_status'] = 'fail'
local_record.preferences['delivery_status_message'] =
message.encode('UTF-8', 'UTF-8', invalid: :replace, replace: '?')
local_record.preferences['delivery_status_date'] = Time.zone.now
local_record.save
Rails.logger.error message
if local_record.preferences['delivery_retry'] > 3
::Ticket::Article.create(
ticket_id: local_record.ticket_id,
content_type: 'text/plain',
body: "Unable to send whatsapp message: #{message}",
internal: true,
sender: ::Ticket::Article::Sender.find_by(name: 'System'),
type: ::Ticket::Article::Type.find_by(name: 'note'),
preferences: {
delivery_article_id_related: local_record.id,
delivery_message: true
},
updated_by_id: 1,
created_by_id: 1
)
end
raise message
end
def max_attempts
4
end
def reschedule_at(current_time, attempts)
return current_time + attempts * 120.seconds if Rails.env.production?
current_time + 5.seconds
end
end
end
end
end
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Controllers
class ChannelsCdrSignalControllerPolicy < Controllers::ApplicationControllerPolicy
default_permit!('admin.channel_cdr_signal')
end
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Controllers
class ChannelsCdrVoiceControllerPolicy < Controllers::ApplicationControllerPolicy
default_permit!('admin.channel_cdr_voice')
end
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
module Controllers
class ChannelsCdrWhatsappControllerPolicy < Controllers::ApplicationControllerPolicy
default_permit!('admin.channel_cdr_whatsapp')
end
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
Rails.application.config.after_initialize do
Ticket::Article.add_observer Observer::Ticket::Article::CommunicateCdrSignal.instance
icon = File.read('public/assets/images/icons/cdr_signal.svg')
doc = File.open('public/assets/images/icons.svg') { |f| Nokogiri::XML(f) }
if !doc.at_css('#icon-cdr-signal')
doc.at('svg').add_child(icon)
Rails.logger.debug 'signal icon added to icon set'
else
Rails.logger.debug 'signal icon already in icon set'
end
File.write('public/assets/images/icons.svg', doc.to_xml)
end

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
Rails.application.config.after_initialize do
Ticket::Article.add_observer Observer::Ticket::Article::CommunicateCdrWhatsapp.instance
icon = File.read('public/assets/images/icons/cdr_whatsapp.svg')
doc = File.open('public/assets/images/icons.svg') { |f| Nokogiri::XML(f) }
if !doc.at_css('#icon-cdr-whatsapp')
doc.at('svg').add_child(icon)
Rails.logger.debug 'whatsapp icon added to icon set'
else
Rails.logger.debug 'whatsapp icon already in icon set'
end
File.write('public/assets/images/icons.svg', doc.to_xml)
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
Zammad::Application.routes.draw do
api_path = Rails.configuration.api_path
match "#{api_path}/channels_cdr_signal", to: 'channels_cdr_signal#index', via: :get
match "#{api_path}/channels_cdr_signal", to: 'channels_cdr_signal#add', via: :post
match "#{api_path}/channels_cdr_signal/:id", to: 'channels_cdr_signal#update', via: :put
match "#{api_path}/channels_cdr_signal_webhook/:token", to: 'channels_cdr_signal#webhook', via: :post
match "#{api_path}/channels_cdr_signal_disable", to: 'channels_cdr_signal#disable', via: :post
match "#{api_path}/channels_cdr_signal_enable", to: 'channels_cdr_signal#enable', via: :post
match "#{api_path}/channels_cdr_signal", to: 'channels_cdr_signal#destroy', via: :delete
match "#{api_path}/channels_cdr_signal_rotate_token", to: 'channels_cdr_signal#rotate_token', via: :post
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
Zammad::Application.routes.draw do
api_path = Rails.configuration.api_path
match "#{api_path}/channels_cdr_voice", to: 'channels_cdr_voice#index', via: :get
match "#{api_path}/channels_cdr_voice", to: 'channels_cdr_voice#add', via: :post
match "#{api_path}/channels_cdr_voice/:id", to: 'channels_cdr_voice#update', via: :put
match "#{api_path}/channels_cdr_voice_webhook/:token", to: 'channels_cdr_voice#webhook', via: :post
match "#{api_path}/channels_cdr_voice_disable", to: 'channels_cdr_voice#disable', via: :post
match "#{api_path}/channels_cdr_voice_enable", to: 'channels_cdr_voice#enable', via: :post
match "#{api_path}/channels_cdr_voice", to: 'channels_cdr_voice#destroy', via: :delete
match "#{api_path}/channels_cdr_voice_rotate_token", to: 'channels_cdr_voice#rotate_token', via: :post
end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
Zammad::Application.routes.draw do
api_path = Rails.configuration.api_path
match "#{api_path}/channels_cdr_whatsapp", to: 'channels_cdr_whatsapp#index', via: :get
match "#{api_path}/channels_cdr_whatsapp", to: 'channels_cdr_whatsapp#add', via: :post
match "#{api_path}/channels_cdr_whatsapp/:id", to: 'channels_cdr_whatsapp#update', via: :put
match "#{api_path}/channels_cdr_whatsapp_webhook/:token", to: 'channels_cdr_whatsapp#webhook', via: :post
match "#{api_path}/channels_cdr_whatsapp_disable", to: 'channels_cdr_whatsapp#disable', via: :post
match "#{api_path}/channels_cdr_whatsapp_enable", to: 'channels_cdr_whatsapp#enable', via: :post
match "#{api_path}/channels_cdr_whatsapp", to: 'channels_cdr_whatsapp#destroy', via: :delete
match "#{api_path}/channels_cdr_whatsapp_rotate_token", to: 'channels_cdr_whatsapp#rotate_token', via: :post
end

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
class CdrSignalChannel < ActiveRecord::Migration[5.2]
def self.up
Ticket::Article::Type.create_if_not_exists(
name: 'cdr_signal',
communication: true,
updated_by_id: 1,
created_by_id: 1
)
Permission.create_if_not_exists(
name: 'admin.channel_cdr_signal',
note: 'Manage %s',
preferences: {
translations: ['Channel - Signal']
}
)
end
def self.down
t = Ticket::Article::Type.find_by(name: 'cdr_signal')
t&.destroy
p = Permission.find_by(name: 'admin.channel_cdr_signal')
p&.destroy
end
end

View file

@ -0,0 +1,28 @@
# frozen_string_literal: true
class CdrVoiceChannel < ActiveRecord::Migration[5.2]
def self.up
Ticket::Article::Type.create_if_not_exists(
name: 'cdr_voice',
communication: false,
updated_by_id: 1,
created_by_id: 1
)
Permission.create_if_not_exists(
name: 'admin.channel_cdr_voice',
note: 'Manage %s',
preferences: {
translations: ['Channel - Voice']
}
)
end
def self.down
t = Ticket::Article::Type.find_by(name: 'cdr_voice')
t&.destroy
p = Permission.find_by(name: 'admin.channel_cdr_voice')
p&.destroy
end
end

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
class CdrWhatsappChannel < ActiveRecord::Migration[5.2]
def self.up
Ticket::Article::Type.create_if_not_exists(
name: 'cdr_whatsapp',
communication: true,
updated_by_id: 1,
created_by_id: 1
)
Permission.create_if_not_exists(
name: 'admin.channel_cdr_whatsapp',
note: 'Manage %s',
preferences: {
translations: ['Channel - Whatsapp']
}
)
end
def self.down
t = Ticket::Article::Type.find_by(name: 'cdr_whatsapp')
t&.destroy
p = Permission.find_by(name: 'admin.channel_cdr_whatsapp')
p&.destroy
end
end

View file

@ -0,0 +1,319 @@
# frozen_string_literal: true
require 'cdr_signal_api'
class CdrSignal
attr_accessor :client
#
# check token and return bot attributes of token
#
# bot = CdrSignal.check_token('token')
#
def self.check_token(api_url, token)
api = CdrSignalAPI.new(api_url, token)
begin
bot = api.fetch_self
rescue StandardError => e
raise "invalid api token: #{e.message}"
end
bot
end
#
# create or update channel, store bot attributes and verify token
#
# channel = CdrSignal.create_or_update_channel('token', params)
#
# returns
#
# channel # instance of Channel
#
def self.create_or_update_channel(api_url, token, params, channel = nil)
# verify token
bot = CdrSignal.check_token(api_url, token)
raise 'Bot already exists!' unless channel && CdrSignal.bot_duplicate?(bot['id'])
raise 'Group needed!' if params[:group_id].blank?
group = Group.find_by(id: params[:group_id])
raise 'Group invalid!' unless group
unless channel
channel = CdrSignal.bot_by_bot_id(bot['id'])
channel ||= Channel.new
end
channel.area = 'Signal::Account'
channel.options = {
adapter: 'cdr_signal',
bot: {
id: bot['id'],
number: bot['number']
},
api_token: token,
api_url: api_url,
welcome: params[:welcome]
}
channel.group_id = group.id
channel.active = true
channel.save!
channel
end
#
# check if bot already exists as channel
#
# success = CdrSignal.bot_duplicate?(bot_id)
#
# returns
#
# channel # instance of Channel
#
def self.bot_duplicate?(bot_id, channel_id = nil)
Channel.where(area: 'Signal::Account').each do |channel|
next unless channel.options
next unless channel.options[:bot]
next unless channel.options[:bot][:id]
next if channel.options[:bot][:id] != bot_id
next if channel.id.to_s == channel_id.to_s
return true
end
false
end
#
# get channel by bot_id
#
# channel = CdrSignal.bot_by_bot_id(bot_id)
#
# returns
#
# true|false
#
def self.bot_by_bot_token(bot_token)
Channel.where(area: 'Signal::Account').each do |channel|
next unless channel.options
next unless channel.options[:bot_token]
return channel if channel.options[:bot_token].to_s == bot_token.to_s
end
nil
end
#
# date = CdrSignal.timestamp_to_date('1543414973285')
#
# returns
#
# 2018-11-28T14:22:53.285Z
#
def self.timestamp_to_date(timestamp_str)
Time.at(timestamp_str.to_i).utc.to_datetime
end
def self.message_id(message_raw)
format('%<source>s@%<timestamp>s', source: message_raw['source'], timestamp: message_raw['timestamp'])
end
#
# client = CdrSignal.new('token')
#
def initialize(api_url, token)
@token = token
@api_url = api_url
@api = CdrSignalAPI.new(api_url, token)
end
#
# client.send_message(chat_id, 'some message')
#
def send_message(recipient, message)
return if Rails.env.test?
@api.send_message(recipient, message)
end
def user(number)
{
# id: params[:message][:from][:id],
id: number,
username: number
# first_name: params[:message][:from][:first_name],
# last_name: params[:message][:from][:last_name]
}
end
def to_user(message)
Rails.logger.debug { 'Create user from message...' }
Rails.logger.debug { message.inspect }
# do message_user lookup
message_user = user(message[:source])
# create or update user
login = message_user[:username] || message_user[:id]
auth = Authorization.find_by(uid: message[:source], provider: 'cdr_signal')
user_data = {
login: login,
mobile: message[:source]
}
user = if auth
User.find(auth.user_id)
else
User.where(mobile: message[:source]).order(:updated_at).first
end
if user
user.update!(user_data)
else
user = User.create!(
firstname: message[:source],
mobile: message[:source],
note: "Signal #{message_user[:username]}",
active: true,
role_ids: Role.signup_role_ids
)
end
# create or update authorization
auth_data = {
uid: message_user[:id],
username: login,
user_id: user.id,
provider: 'cdr_signal'
}
if auth
auth.update!(auth_data)
else
Authorization.create(auth_data)
end
user
end
def to_ticket(message, user, group_id, channel)
UserInfo.current_user_id = user.id
Rails.logger.debug { 'Create ticket from message...' }
Rails.logger.debug { message.inspect }
Rails.logger.debug { user.inspect }
Rails.logger.debug { group_id.inspect }
# prepare title
title = '-'
title = message[:message][:body] unless message[:message][:body].nil?
title = "#{title[0, 60]}..." if title.length > 60
# find ticket or create one
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
ticket = Ticket.where(customer_id: user.id).where.not(state_id: state_ids).order(:updated_at).first
if ticket
# check if title need to be updated
ticket.title = title if ticket.title == '-'
new_state = Ticket::State.find_by(default_create: true)
ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id
ticket.save!
return ticket
end
ticket = Ticket.new(
group_id: group_id,
title: title,
state_id: Ticket::State.find_by(default_create: true).id,
priority_id: Ticket::Priority.find_by(default_create: true).id,
customer_id: user.id,
preferences: {
channel_id: channel.id,
cdr_signal: {
bot_token: channel.options[:bot_token],
chat_id: message[:source]
}
}
)
ticket.save!
ticket
end
def to_article(message, user, ticket, channel)
Rails.logger.debug { 'Create article from message...' }
Rails.logger.debug { message.inspect }
Rails.logger.debug { user.inspect }
Rails.logger.debug { ticket.inspect }
UserInfo.current_user_id = user.id
article = Ticket::Article.new(
from: message[:source],
to: channel[:options][:bot][:number],
body: message[:message][:body],
content_type: 'text/plain',
message_id: "cdr_signal.#{message[:id]}",
ticket_id: ticket.id,
type_id: Ticket::Article::Type.find_by(name: 'cdr_signal').id,
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
internal: false,
preferences: {
cdr_signal: {
timestamp: message[:timestamp],
message_id: message[:id],
from: message[:source]
}
}
)
# TODO: attachments
# TODO voice
# TODO emojis
#
if message[:message][:body]
Rails.logger.debug { article.inspect }
article.save!
Store.remove(
object: 'Ticket::Article',
o_id: article.id
)
return article
end
raise 'invalid action'
end
def to_group(message, group_id, channel)
# begin import
Rails.logger.debug { 'signal import message' }
# TODO: handle messages in group chats
return if Ticket::Article.find_by(message_id: message[:id])
ticket = nil
# use transaction
Transaction.execute(reset_user_id: true) do
user = to_user(message)
ticket = to_ticket(message, user, group_id, channel)
to_article(message, user, ticket, channel)
end
ticket
end
def from_article(article)
# sends a message from a zammad article
Rails.logger.debug { "Create signal message from article to '#{article[:to]}'..." }
@api.send_message(article[:to], article[:body])
end
end

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'json'
require 'net/http'
require 'net/https'
require 'uri'
require 'rest-client'
class CdrSignalAPI
def initialize(api_url, token)
@token = token
@last_update = 0
@api = api_url
end
def parse_hash(hash)
ret = {}
hash.map do |k, v|
ret[k] = CGI.encode(v.to_s.gsub('\\\'', '\''))
end
ret
end
def get(api)
url = "#{@api}/bots/#{@token}/#{api}"
JSON.parse(RestClient.get(url, { accept: :json }).body)
end
def post(api, params = {})
url = "#{@api}/bots/#{@token}/#{api}"
JSON.parse(RestClient.post(url, params, { accept: :json }).body)
end
def fetch_self
get('')
end
def send_message(recipient, text, options = {})
post('send', { phoneNumber: recipient.to_s, message: text }.merge(parse_hash(options)))
end
end

View file

@ -0,0 +1,319 @@
# frozen_string_literal: true
require 'cdr_whatsapp_api'
class CdrWhatsapp
attr_accessor :client
#
# check token and return bot attributes of token
#
# bot = CdrWhatsapp.check_token('token')
#
def self.check_token(api_url, token)
api = CdrWhatsappAPI.new(api_url, token)
begin
bot = api.fetch_self
rescue StandardError => e
raise "invalid api token: #{e.message}"
end
bot
end
#
# create or update channel, store bot attributes and verify token
#
# channel = CdrWhatsapp.create_or_update_channel('token', params)
#
# returns
#
# channel # instance of Channel
#
def self.create_or_update_channel(api_url, token, params, channel = nil)
# verify token
bot = CdrWhatsapp.check_token(api_url, token)
raise 'Bot already exists!' unless channel && CdrWhatsapp.bot_duplicate?(bot['id'])
raise 'Group needed!' if params[:group_id].blank?
group = Group.find_by(id: params[:group_id])
raise 'Group invalid!' unless group
unless channel
channel = CdrWhatsapp.bot_by_bot_id(bot['id'])
channel ||= Channel.new
end
channel.area = 'Whatsapp::Account'
channel.options = {
adapter: 'cdr_whatsapp',
bot: {
id: bot['id'],
number: bot['number']
},
api_token: token,
api_url: api_url,
welcome: params[:welcome]
}
channel.group_id = group.id
channel.active = true
channel.save!
channel
end
#
# check if bot already exists as channel
#
# success = CdrWhatsapp.bot_duplicate?(bot_id)
#
# returns
#
# channel # instance of Channel
#
def self.bot_duplicate?(bot_id, channel_id = nil)
Channel.where(area: 'Whatsapp::Account').each do |channel|
next unless channel.options
next unless channel.options[:bot]
next unless channel.options[:bot][:id]
next if channel.options[:bot][:id] != bot_id
next if channel.id.to_s == channel_id.to_s
return true
end
false
end
#
# get channel by bot_id
#
# channel = CdrWhatsapp.bot_by_bot_id(bot_id)
#
# returns
#
# true|false
#
def self.bot_by_bot_token(bot_token)
Channel.where(area: 'Whatsapp::Account').each do |channel|
next unless channel.options
next unless channel.options[:bot_token]
return channel if channel.options[:bot_token].to_s == bot_token.to_s
end
nil
end
#
# date = CdrWhatsapp.timestamp_to_date('1543414973285')
#
# returns
#
# 2018-11-28T14:22:53.285Z
#
def self.timestamp_to_date(timestamp_str)
Time.at(timestamp_str.to_i).utc.to_datetime
end
def self.message_id(message_raw)
format('%<source>s@%<timestamp>s', source: message_raw['source'], timestamp: message_raw['timestamp'])
end
#
# client = CdrWhatsapp.new('token')
#
def initialize(api_url, token)
@token = token
@api_url = api_url
@api = CdrWhatsappAPI.new(api_url, token)
end
#
# client.send_message(chat_id, 'some message')
#
def send_message(recipient, message)
return if Rails.env.test?
@api.send_message(recipient, message)
end
def user(number)
{
# id: params[:message][:from][:id],
id: number,
username: number
# first_name: params[:message][:from][:first_name],
# last_name: params[:message][:from][:last_name]
}
end
def to_user(message)
Rails.logger.debug { 'Create user from message...' }
Rails.logger.debug { message.inspect }
# do message_user lookup
message_user = user(message[:source])
# create or update user
login = message_user[:username] || message_user[:id]
auth = Authorization.find_by(uid: message[:source], provider: 'whatsapp')
user_data = {
login: login,
mobile: message[:source]
}
user = if auth
User.find(auth.user_id)
else
User.where(mobile: message[:source]).order(:updated_at).first
end
if user
user.update!(user_data)
else
user = User.create!(
firstname: message[:source],
mobile: message[:source],
note: "Whatsapp #{message_user[:username]}",
active: true,
role_ids: Role.signup_role_ids
)
end
# create or update authorization
auth_data = {
uid: message_user[:id],
username: login,
user_id: user.id,
provider: 'cdr_whatsapp'
}
if auth
auth.update!(auth_data)
else
Authorization.create(auth_data)
end
user
end
def to_ticket(message, user, group_id, channel)
UserInfo.current_user_id = user.id
Rails.logger.debug { 'Create ticket from message...' }
Rails.logger.debug { message.inspect }
Rails.logger.debug { user.inspect }
Rails.logger.debug { group_id.inspect }
# prepare title
title = '-'
title = message[:message][:body] unless message[:message][:body].nil?
title = "#{title[0, 60]}..." if title.length > 60
# find ticket or create one
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
ticket = Ticket.where(customer_id: user.id).where.not(state_id: state_ids).order(:updated_at).first
if ticket
# check if title need to be updated
ticket.title = title if ticket.title == '-'
new_state = Ticket::State.find_by(default_create: true)
ticket.state = Ticket::State.find_by(default_follow_up: true) if ticket.state_id != new_state.id
ticket.save!
return ticket
end
ticket = Ticket.new(
group_id: group_id,
title: title,
state_id: Ticket::State.find_by(default_create: true).id,
priority_id: Ticket::Priority.find_by(default_create: true).id,
customer_id: user.id,
preferences: {
channel_id: channel.id,
cdr_whatsapp: {
bot_id: channel.options[:bot][:id],
chat_id: message[:source]
}
}
)
ticket.save!
ticket
end
def to_article(message, user, ticket, channel)
Rails.logger.debug { 'Create article from message...' }
Rails.logger.debug { message.inspect }
Rails.logger.debug { user.inspect }
Rails.logger.debug { ticket.inspect }
UserInfo.current_user_id = user.id
article = Ticket::Article.new(
from: message[:source],
to: channel[:options][:bot][:number],
body: message[:message][:body],
content_type: 'text/plain',
message_id: "cdr_whatsapp.#{message[:id]}",
ticket_id: ticket.id,
type_id: Ticket::Article::Type.find_by(name: 'cdr_whatsapp').id,
sender_id: Ticket::Article::Sender.find_by(name: 'Customer').id,
internal: false,
preferences: {
cdr_whatsapp: {
timestamp: message[:timestamp],
message_id: message[:id],
from: message[:source]
}
}
)
# TODO: attachments
# TODO voice
# TODO emojis
#
if message[:message][:body]
Rails.logger.debug { article.inspect }
article.save!
Store.remove(
object: 'Ticket::Article',
o_id: article.id
)
return article
end
raise 'invalid action'
end
def to_group(message, group_id, channel)
# begin import
Rails.logger.debug { 'whatsapp import message' }
# TODO: handle messages in group chats
return if Ticket::Article.find_by(message_id: message[:id])
ticket = nil
# use transaction
Transaction.execute(reset_user_id: true) do
user = to_user(message)
ticket = to_ticket(message, user, group_id, channel)
to_article(message, user, ticket, channel)
end
ticket
end
def from_article(article)
# sends a message from a zammad article
Rails.logger.debug { "Create whatsapp message from article to '#{article[:to]}'..." }
@api.send_message(article[:to], article[:body])
end
end

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'json'
require 'net/http'
require 'net/https'
require 'uri'
require 'rest-client'
class CdrWhatsappAPI
def initialize(api_url, token)
@token = token
@last_update = 0
@api_url = api_url
end
def parse_hash(hash)
ret = {}
hash.map do |k, v|
ret[k] = CGI.encode(v.to_s.gsub('\\\'', '\''))
end
ret
end
def get(api)
url = "#{@api_url}/bots/#{@token}/#{api}"
JSON.parse(RestClient.get(url, { accept: :json }).body)
end
def post(api, params = {})
url = "#{@api_url}/bots/#{@token}/#{api}"
JSON.parse(RestClient.post(url, params, { accept: :json }).body)
end
def fetch_self
get('')
end
def send_message(recipient, text, options = {})
post('send', { phoneNumber: recipient.to_s, message: text }.merge(parse_hash(options)))
end
end

View file

@ -0,0 +1,3 @@
<symbol id="icon-cdr-signal" viewBox="0 0 17 17"><title>signal</title>
<defs><path id="a" d="M1 .41h.925v2.167H1z"/><path id="c" d="M.356 1h2.179v.745H.355z"/><path id="e" d="M.935.432h.921v2.167h-.92z"/><path id="g" d="M1 .202h.838V2.37H1z"/><path id="i" d="M1 .95h.856v2.165H.999z"/><path id="k" d="M.09.605H1.62V2H.09z"/></defs><g fill="none" fill-rule="evenodd"><path d="M15.255 7.872c0-3.422-2.988-6.196-6.675-6.196-3.686 0-6.717 2.774-6.717 6.196 0 1.925.59 3.806 2.18 4.913l.133 1.986a.327.327 0 0 0 .497.258c.703-.427 1.966-1.19 2.005-1.18 5.168 1.121 8.577-2.555 8.577-5.977" fill="#C6C7C8"/><g transform="translate(-1 5)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M1.842.555l-.4-.145a7.466 7.466 0 0 0-.434 2.145l.426.022c.036-.69.174-1.37.408-2.022" fill="#C6C7C8" mask="url(#b)"/></g><path d="M6.791.59L6.708.17A8.732 8.732 0 0 0 4.64.858l.183.385A8.358 8.358 0 0 1 6.792.59M4.272 1.533l-.213-.37a8.43 8.43 0 0 0-1.736 1.322l.3.302a8.05 8.05 0 0 1 1.649-1.254M2.199 3.243l-.324-.278A7.886 7.886 0 0 0 .69 4.801l.388.18a7.477 7.477 0 0 1 1.12-1.738" fill="#C6C7C8"/><g transform="translate(7 -1)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M2.484 1.489l.05-.424a9.026 9.026 0 0 0-2.178.002l.052.423a8.634 8.634 0 0 1 2.076-.001" fill="#C6C7C8" mask="url(#d)"/></g><path d="M16.18 4.822a7.854 7.854 0 0 0-1.171-1.845l-.325.276a7.45 7.45 0 0 1 1.107 1.745l.389-.176z" fill="#C6C7C8"/><g transform="translate(15 5)"><mask id="f" fill="#fff"><use xlink:href="#e"/></mask><path d="M1.42 2.599l.427-.021A7.568 7.568 0 0 0 1.425.432l-.402.143c.23.652.364 1.333.397 2.024" fill="#C6C7C8" mask="url(#f)"/></g><path d="M14.564 2.493a8.426 8.426 0 0 0-1.731-1.33l-.214.37c.604.35 1.157.774 1.643 1.262l.302-.302zM12.252.857a8.653 8.653 0 0 0-2.07-.688L10.1.587a8.23 8.23 0 0 1 1.966.654l.186-.384zM12.98 14.214l.229.361a7.813 7.813 0 0 0 1.668-1.41l-.318-.284a7.376 7.376 0 0 1-1.578 1.333" fill="#C6C7C8"/><g transform="translate(-1 8)"><mask id="h" fill="#fff"><use xlink:href="#g"/></mask><path d="M1.426.202L1 .21c.016.773.104 1.499.261 2.16l.415-.1a9.917 9.917 0 0 1-.25-2.068" fill="#C6C7C8" mask="url(#h)"/></g><path d="M10.495 15.233l.095.415a8.604 8.604 0 0 0 2.049-.746l-.198-.378c-.6.312-1.257.55-1.947.709M8.314 15.47h-.001c-.17 0-.343-.004-.516-.012l-.02.426a10.49 10.49 0 0 0 2.169-.112l-.066-.422c-.503.08-1.03.12-1.566.12" fill="#C6C7C8"/><g transform="translate(15 7)"><mask id="j" fill="#fff"><use xlink:href="#i"/></mask><path d="M1.43.95c0 .692-.098 1.38-.288 2.048l.41.117A7.892 7.892 0 0 0 1.856.95H1.43z" fill="#C6C7C8" mask="url(#j)"/></g><path d="M14.953 12.4l.34.255a7.809 7.809 0 0 0 1.053-1.916l-.4-.152a7.374 7.374 0 0 1-.993 1.813M.44 11.002a6.393 6.393 0 0 0 1.016 1.946l.337-.26a5.979 5.979 0 0 1-.948-1.819l-.405.133zM6.653 15.353c-.058 0-.082 0-1.604.624l.163.395a48.799 48.799 0 0 1 1.46-.588c.152.021.306.038.457.053l.042-.424a13.84 13.84 0 0 1-.49-.058c-.008-.002-.018-.002-.028-.002M2.94 13.828a.215.215 0 0 0-.078-.109 5.416 5.416 0 0 1-.665-.57l-.304.3c.204.206.426.4.66.573l.346 1.1.407-.129-.366-1.165z" fill="#C6C7C8"/><g transform="translate(3 15)"><mask id="l" fill="#fff"><use xlink:href="#k"/></mask><path d="M.779 1.5L.498.606.09.732.44 1.85A.219.219 0 0 0 .645 2a.203.203 0 0 0 .08-.017l.894-.368-.163-.395-.677.28" fill="#C6C7C8" mask="url(#l)"/></g></g>
</symbol>

View file

@ -0,0 +1,6 @@
<symbol id="icon-cdr-whatsapp" viewBox="0 0 17 17"><title>whatsapp</title>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><g transform="translate(0.000000,462.000000) scale(0.100000,-0.100000)"><path d="M4472.9,4492.4C2377.1,4206.7,746.1,2682.3,347.4,637.7c-198.3-1025.5-51.2-2070.2,428.5-3036l172.7-343.2L520.1-4005.9c-236.7-692.9-424.3-1266.4-420-1270.7c4.3-4.3,601.2,179.1,1324,409.4l1317.6,420l251.6-119.4c1029.8-481.8,2172.6-599.1,3259.9-330.5c1669.4,413.6,3012.6,1709.9,3475.3,3355.8c194,695,226,1522.3,83.2,2228c-328.3,1618.2-1483.9,2963.6-3048.8,3545.6c-236.7,89.6-622.6,189.8-914.7,240.9C5568.8,4520.1,4760.7,4532.9,4472.9,4492.4z M5666.9,3722.8c1571.3-213.2,2906-1383.7,3326-2918.8c179.1-654.6,187.6-1385.8,23.4-2034c-336.9-1326.1-1341-2402.8-2645.9-2835.6c-1115.1-371-2351.7-243-3343.1,347.5c-91.7,53.3-174.8,98.1-183.4,98.1c-6.4,0-356.1-108.8-776.1-243.1c-417.9-132.2-761.1-234.5-761.1-223.9c0,8.5,110.9,343.3,247.3,742l247.3,724.9l-110.9,166.3c-138.6,208.9-362.5,663.1-445.6,904C594.7,334.9,1453.9,2420.1,3257.6,3332.6C4001.7,3707.8,4809.8,3837.9,5666.9,3722.8z"/><path d="M3272.6,1878.5c-168.4-83.1-400.8-407.2-486.1-678c-17.1-53.3-38.4-194-44.8-311.3c-14.9-272.9,38.4-509.6,185.5-816.6c108.7-226,360.3-605.5,609.8-916.8c437.1-547.9,976.5-995.7,1488.2-1240.9c398.7-189.8,1012.7-392.3,1272.8-420c356.1-36.3,897.6,200.4,1087.4,477.6c123.7,179.1,191.9,618.3,108.7,695c-42.6,38.4-850.7,432.8-1010.6,494.6c-55.4,21.3-123.7,32-151.4,23.4c-32-6.4-115.1-91.7-209-208.9C5892.9-1313.1,5779.9-1424,5718-1424c-83.1,0-496.8,200.4-729.1,353.9c-296.4,196.2-678,579.9-884.8,889.1c-85.3,125.8-153.5,253.7-153.5,281.4c0,29.9,55.4,113,149.2,221.7c136.4,159.9,255.8,349.6,255.8,409.4c0,12.8-98.1,260.1-217.5,550.1c-140.7,336.9-238.8,545.8-275,584.2c-55.4,55.4-66.1,57.6-279.3,57.6C3402.6,1923.3,3347.2,1914.8,3272.6,1878.5z"/></g></g>
</svg>
</symbol>