Initial SMS integration for trigger notifications and additional channel. Thanks to sys4 AG!
This commit is contained in:
parent
271102057d
commit
22b2f44ba0
60 changed files with 2336 additions and 335 deletions
3
Gemfile
3
Gemfile
|
@ -105,6 +105,9 @@ gem 'icalendar-recurrence'
|
|||
# feature - phone number formatting
|
||||
gem 'telephone_number'
|
||||
|
||||
# feature - SMS
|
||||
gem 'twilio-ruby'
|
||||
|
||||
# integrations
|
||||
gem 'clearbit'
|
||||
gem 'net-ldap'
|
||||
|
|
|
@ -447,6 +447,10 @@ GEM
|
|||
thread_safe (0.3.6)
|
||||
tilt (2.0.8)
|
||||
tins (1.15.1)
|
||||
twilio-ruby (5.10.2)
|
||||
faraday (~> 0.9)
|
||||
jwt (>= 1.5, <= 2.5)
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
twitter (6.2.0)
|
||||
addressable (~> 2.3)
|
||||
buftok (~> 0.2.0)
|
||||
|
@ -578,6 +582,7 @@ DEPENDENCIES
|
|||
telephone_number
|
||||
test-unit
|
||||
therubyracer
|
||||
twilio-ruby
|
||||
twitter
|
||||
uglifier
|
||||
unicorn
|
||||
|
|
|
@ -25,7 +25,8 @@ class App.ControllerForm extends App.Controller
|
|||
@form = @formGen()
|
||||
|
||||
# add alert placeholder
|
||||
@form.prepend('<div class="alert alert--danger js-alert hide" role="alert"></div>')
|
||||
@form.prepend('<div class="alert alert--danger js-danger js-alert hide" role="alert"></div>')
|
||||
@form.prepend('<div class="alert alert--success js-success hide" role="alert"></div>')
|
||||
|
||||
# if element is given, prepend form to it
|
||||
if @el
|
||||
|
|
|
@ -295,15 +295,15 @@ class App.ControllerGenericDestroyConfirm extends App.ControllerModal
|
|||
App.i18n.translateContent('Sure to delete this object?')
|
||||
|
||||
onSubmit: =>
|
||||
@item.destroy(
|
||||
done: =>
|
||||
@close()
|
||||
if @callback
|
||||
@callback()
|
||||
fail: =>
|
||||
@log 'errors'
|
||||
@close()
|
||||
)
|
||||
options = @options || {}
|
||||
options.done = =>
|
||||
@close()
|
||||
if @callback
|
||||
@callback()
|
||||
options.fail = =>
|
||||
@log 'errors'
|
||||
@close()
|
||||
@item.destroy(options)
|
||||
|
||||
class App.ControllerConfirm extends App.ControllerModal
|
||||
buttonClose: true
|
||||
|
|
|
@ -67,7 +67,7 @@ class App.ChannelEmailFilter extends App.Controller
|
|||
container: @el.closest('.content')
|
||||
callback: @load
|
||||
)
|
||||
|
||||
|
||||
edit: (id, e) =>
|
||||
e.preventDefault()
|
||||
new App.ControllerGenericEdit(
|
||||
|
|
442
app/assets/javascripts/app/controllers/_channel/sms.coffee
Normal file
442
app/assets/javascripts/app/controllers/_channel/sms.coffee
Normal file
|
@ -0,0 +1,442 @@
|
|||
class App.ChannelSms extends App.ControllerTabs
|
||||
requiredPermission: 'admin.channel_sms'
|
||||
header: 'SMS'
|
||||
constructor: ->
|
||||
super
|
||||
|
||||
@title 'SMS', true
|
||||
@tabs = [
|
||||
{
|
||||
name: 'Accounts',
|
||||
target: 'c-account',
|
||||
controller: App.ChannelSmsAccountOverview,
|
||||
},
|
||||
]
|
||||
|
||||
@render()
|
||||
|
||||
class App.ChannelSmsAccountOverview extends App.Controller
|
||||
events:
|
||||
'click .js-channelEdit': 'change'
|
||||
'click .js-channelDelete': 'delete'
|
||||
'click .js-channelDisable': 'disable'
|
||||
'click .js-channelEnable': 'enable'
|
||||
'click .js-editNotification': 'editNotification'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@interval(@load, 30000)
|
||||
#@load()
|
||||
|
||||
load: =>
|
||||
|
||||
@startLoading()
|
||||
|
||||
@ajax(
|
||||
id: 'sms_index'
|
||||
type: 'GET'
|
||||
url: "#{@apiPath}/channels_sms"
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
@config = data.config
|
||||
@stopLoading()
|
||||
App.Collection.loadAssets(data.assets)
|
||||
@render(data)
|
||||
)
|
||||
|
||||
render: (data = {}) =>
|
||||
|
||||
@channelDriver = data.channel_driver
|
||||
|
||||
# get channels
|
||||
@account_channels = []
|
||||
for channel_id in data.account_channel_ids
|
||||
account_channel = App.Channel.fullLocal(channel_id)
|
||||
if account_channel.group_id
|
||||
account_channel.group = App.Group.find(account_channel.group_id)
|
||||
else
|
||||
account_channel.group = '-'
|
||||
@account_channels.push account_channel
|
||||
|
||||
# get channels
|
||||
@notification_channels = []
|
||||
for channel_id in data.notification_channel_ids
|
||||
@notification_channels.push App.Channel.find(channel_id)
|
||||
|
||||
@html App.view('channel/sms_account_overview')(
|
||||
account_channels: @account_channels
|
||||
notification_channels: @notification_channels
|
||||
config: @config
|
||||
)
|
||||
|
||||
change: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
if !id
|
||||
channel = new App.Channel(active: true)
|
||||
else
|
||||
channel = App.Channel.find(id)
|
||||
new App.ChannelSmsAccount(
|
||||
container: @el.closest('.content')
|
||||
channel: channel
|
||||
callback: @load
|
||||
channelDriver: @channelDriver
|
||||
config: @config
|
||||
)
|
||||
|
||||
delete: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
channel = App.Channel.find(id)
|
||||
new App.ControllerGenericDestroyConfirm(
|
||||
item: channel
|
||||
options:
|
||||
url: "/api/v1/channels_sms/#{channel.id}"
|
||||
container: @el.closest('.content')
|
||||
callback: =>
|
||||
@load()
|
||||
)
|
||||
|
||||
disable: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
@ajax(
|
||||
id: 'sms_disable'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_sms_disable"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
|
||||
enable: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
@ajax(
|
||||
id: 'sms_enable'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_sms_enable"
|
||||
data: JSON.stringify(id: id)
|
||||
processData: true
|
||||
success: =>
|
||||
@load()
|
||||
)
|
||||
|
||||
editNotification: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
channel = App.Channel.find(id)
|
||||
new App.ChannelSmsNotification(
|
||||
container: @el.closest('.content')
|
||||
channel: channel
|
||||
callback: @load
|
||||
channelDriver: @channelDriver
|
||||
config: @config
|
||||
)
|
||||
|
||||
class App.ChannelSmsAccount extends App.ControllerModal
|
||||
head: 'SMS Account'
|
||||
buttonCancel: true
|
||||
centerButtons: [
|
||||
{
|
||||
text: 'Test'
|
||||
className: 'js-test'
|
||||
}
|
||||
]
|
||||
elements:
|
||||
'form': 'form'
|
||||
'select[name="options::adapter"]': 'adapterSelect'
|
||||
events:
|
||||
'click .js-test': 'onTest'
|
||||
|
||||
content: ->
|
||||
el = $('<div><div class="js-channelAdapterSelector"></div><div class="js-channelWebhook"></div><div class="js-channelAdapterOptions"></div></div>')
|
||||
|
||||
# form
|
||||
options = {}
|
||||
currentConfig = {}
|
||||
for config in @config
|
||||
if config.account
|
||||
options[config.adapter] = config.name
|
||||
|
||||
form = new App.ControllerForm(
|
||||
el: el.find('.js-channelAdapterSelector')
|
||||
model:
|
||||
configure_attributes: [
|
||||
{ name: 'options::adapter', display: 'Provider', tag: 'select', null: false, options: options, nulloption: true }
|
||||
]
|
||||
className: ''
|
||||
params: @channel
|
||||
)
|
||||
@renderAdapterOptions(@channel.options?.adapter, el)
|
||||
el.find('[name="options::adapter"]').bind('change', (e) =>
|
||||
@renderAdapterOptions(e.target.value, el)
|
||||
)
|
||||
el
|
||||
|
||||
renderAdapterOptions: (adapter, el) ->
|
||||
el.find('.js-channelWebhook').html('')
|
||||
el.find('.js-channelAdapterOptions').html('')
|
||||
|
||||
currentConfig = {}
|
||||
for configuration in @config
|
||||
if configuration.adapter is adapter
|
||||
if configuration.account
|
||||
currentConfig = configuration.account
|
||||
return if _.isEmpty(currentConfig)
|
||||
|
||||
if _.isEmpty(@channel.options) || _.isEmpty(@channel.options.webhook_token)
|
||||
@channel.options ||= {}
|
||||
@channel.options.webhook_token = '?'
|
||||
for localCurrentConfig in currentConfig
|
||||
if localCurrentConfig.name is 'options::webhook_token'
|
||||
@channel.options.webhook_token = localCurrentConfig.default
|
||||
|
||||
webhook = "#{@Config.get('http_type')}://#{@Config.get('fqdn')}/api/v1/sms_webhook/#{@channel.options?.webhook_token}"
|
||||
new App.ControllerForm(
|
||||
el: el.find('.js-channelWebhook')
|
||||
model:
|
||||
configure_attributes: [
|
||||
{ name: 'options::webhook', display: 'Webhook', tag: 'input', type: 'text', limit: 200, null: false, default: webhook, disabled: true },
|
||||
]
|
||||
className: ''
|
||||
params: @channel
|
||||
)
|
||||
|
||||
new App.ControllerForm(
|
||||
el: el.find('.js-channelAdapterOptions')
|
||||
model:
|
||||
configure_attributes: currentConfig
|
||||
className: ''
|
||||
params: @channel
|
||||
)
|
||||
|
||||
onDelete: =>
|
||||
if @channel.isNew() is true
|
||||
@close()
|
||||
@callback()
|
||||
return
|
||||
|
||||
new App.ControllerGenericDestroyConfirm(
|
||||
item: @channel
|
||||
options:
|
||||
url: "/api/v1/channels_sms/#{@channel.id}"
|
||||
container: @el.closest('.content')
|
||||
callback: =>
|
||||
@close()
|
||||
@callback()
|
||||
)
|
||||
|
||||
onSubmit: (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
if @adapterSelect.val() is ''
|
||||
@onDelete()
|
||||
return
|
||||
|
||||
@formDisable(e)
|
||||
|
||||
@channel.options ||= {}
|
||||
for key, value of @formParam(@el)
|
||||
if key is 'options'
|
||||
for optionsKey, optionsValue of value
|
||||
@channel.options ||= {}
|
||||
@channel.options[optionsKey] = optionsValue
|
||||
else
|
||||
@channel[key] = value
|
||||
@channel.area = 'Sms::Account'
|
||||
|
||||
url = '/api/v1/channels_sms'
|
||||
if !@channel.isNew()
|
||||
url = "/api/v1/channels_sms/#{@channel.id}"
|
||||
|
||||
ui = @
|
||||
@channel.save(
|
||||
url: url
|
||||
done: ->
|
||||
ui.formEnable(e)
|
||||
ui.channel = App.Channel.find(@id)
|
||||
ui.close()
|
||||
ui.callback()
|
||||
fail: (settings, details) ->
|
||||
ui.log 'errors', details
|
||||
ui.formEnable(e)
|
||||
ui.showAlert(details.error_human || details.error || 'Unable to update object!')
|
||||
)
|
||||
|
||||
onTest: (e) ->
|
||||
e.preventDefault()
|
||||
new TestModal(
|
||||
channel: @formParam(@el)
|
||||
container: @el.closest('.content')
|
||||
)
|
||||
|
||||
class App.ChannelSmsNotification extends App.ControllerModal
|
||||
head: 'SMS Notification'
|
||||
buttonCancel: true
|
||||
centerButtons: [
|
||||
{
|
||||
text: 'Test'
|
||||
className: 'js-test'
|
||||
}
|
||||
]
|
||||
elements:
|
||||
'form': 'form'
|
||||
'select[name="options::adapter"]': 'adapterSelect'
|
||||
events:
|
||||
'click .js-test': 'onTest'
|
||||
|
||||
content: ->
|
||||
el = $('<div><div class="js-channelAdapterSelector"></div><div class="js-channelAdapterOptions"></div></div>')
|
||||
if !@channel
|
||||
@channel = new App.Channel(active: true)
|
||||
|
||||
# form
|
||||
options = {}
|
||||
currentConfig = {}
|
||||
for config in @config
|
||||
if config.notification
|
||||
options[config.adapter] = config.name
|
||||
|
||||
form = new App.ControllerForm(
|
||||
el: el.find('.js-channelAdapterSelector')
|
||||
model:
|
||||
configure_attributes: [
|
||||
{ name: 'options::adapter', display: 'Provider', tag: 'select', null: false, options: options, nulloption: true }
|
||||
]
|
||||
className: ''
|
||||
params: @channel
|
||||
)
|
||||
@renderAdapterOptions(@channel.options?.adapter, el)
|
||||
el.find('[name="options::adapter"]').bind('change', (e) =>
|
||||
@renderAdapterOptions(e.target.value, el)
|
||||
)
|
||||
el
|
||||
|
||||
renderAdapterOptions: (adapter, el) ->
|
||||
el.find('.js-channelAdapterOptions').html('')
|
||||
|
||||
currentConfig = {}
|
||||
for configuration in @config
|
||||
if configuration.adapter is adapter
|
||||
if configuration.notification
|
||||
currentConfig = configuration.notification
|
||||
return if _.isEmpty(currentConfig)
|
||||
|
||||
new App.ControllerForm(
|
||||
el: el.find('.js-channelAdapterOptions')
|
||||
model:
|
||||
configure_attributes: currentConfig
|
||||
className: ''
|
||||
params: @channel
|
||||
)
|
||||
|
||||
onDelete: =>
|
||||
if @channel.isNew() is true
|
||||
@close()
|
||||
@callback()
|
||||
return
|
||||
|
||||
new App.ControllerGenericDestroyConfirm(
|
||||
item: @channel
|
||||
options:
|
||||
url: "/api/v1/channels_sms/#{@channel.id}"
|
||||
container: @el.closest('.content')
|
||||
callback: =>
|
||||
@close()
|
||||
@callback()
|
||||
)
|
||||
|
||||
onSubmit: (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
if @adapterSelect.val() is ''
|
||||
@onDelete()
|
||||
return
|
||||
|
||||
@formDisable(e)
|
||||
|
||||
@channel.options ||= {}
|
||||
for key, value of @formParam(@el)
|
||||
@channel[key] = value
|
||||
@channel.area = 'Sms::Notification'
|
||||
|
||||
url = '/api/v1/channels_sms'
|
||||
if !@channel.isNew()
|
||||
url = "/api/v1/channels_sms/#{@channel.id}"
|
||||
ui = @
|
||||
@channel.save(
|
||||
url: url
|
||||
done: ->
|
||||
ui.formEnable(e)
|
||||
ui.channel = App.Channel.find(@id)
|
||||
ui.close()
|
||||
ui.callback()
|
||||
fail: (settings, details) ->
|
||||
ui.log 'errors', details
|
||||
ui.formEnable(e)
|
||||
ui.showAlert(details.error_human || details.error || 'Unable to update object!')
|
||||
)
|
||||
|
||||
onTest: (e) ->
|
||||
e.preventDefault()
|
||||
new TestModal(
|
||||
channel: @formParam(@el)
|
||||
container: @el.closest('.content')
|
||||
)
|
||||
|
||||
class TestModal extends App.ControllerModal
|
||||
head: 'Test SMS provider'
|
||||
buttonCancel: true
|
||||
|
||||
content: ->
|
||||
form = new App.ControllerForm(
|
||||
model:
|
||||
configure_attributes: [
|
||||
{ name: 'recipient', display: 'Recipient', tag: 'input', null: false }
|
||||
{ name: 'message', display: 'Message', tag: 'input', null: false, default: 'Test message from Zammad' }
|
||||
]
|
||||
className: ''
|
||||
)
|
||||
form.form
|
||||
|
||||
T: (name) ->
|
||||
App.i18n.translateInline(name)
|
||||
|
||||
submit: (e) ->
|
||||
super(e)
|
||||
|
||||
@el.find('.js-danger').addClass('hide')
|
||||
@el.find('.js-success').addClass('hide')
|
||||
@formDisable(@el)
|
||||
|
||||
testData = _.extend(
|
||||
@formParam(e.currentTarget),
|
||||
options: @channel.options
|
||||
)
|
||||
|
||||
@ajax(
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/channels_sms/test"
|
||||
data: JSON.stringify(testData)
|
||||
processData: true
|
||||
success: (data) =>
|
||||
@formEnable(@el)
|
||||
if error_text = (data.error || data.error_human)
|
||||
@el.find('.js-danger')
|
||||
.text(@T(error_text))
|
||||
.removeClass('hide')
|
||||
else
|
||||
@el.find('.js-success')
|
||||
.text(@T('SMS successfully sent'))
|
||||
.removeClass('hide')
|
||||
error: (xhr) =>
|
||||
data = JSON.parse(xhr.responseText)
|
||||
@formEnable(@el)
|
||||
@el.find('.js-danger')
|
||||
.text(@T(data.error || 'Unable to perform test'))
|
||||
.removeClass('hide')
|
||||
)
|
||||
|
||||
App.Config.set('SMS', { prio: 3100, name: 'SMS', parent: '#channels', target: '#channels/sms', controller: App.ChannelSms, permission: ['admin.channel_sms'] }, 'NavBarAdmin')
|
|
@ -18,6 +18,7 @@ class App.UiElement.ticket_perform_action
|
|||
for groupKey, groupMeta of groups
|
||||
if !groupMeta.model || !App[groupMeta.model]
|
||||
elements["#{groupKey}.email"] = { name: 'email', display: 'Email' }
|
||||
elements["#{groupKey}.sms"] = { name: 'sms', display: 'SMS' }
|
||||
else
|
||||
|
||||
for row in App[groupMeta.model].configure_attributes
|
||||
|
@ -161,9 +162,11 @@ class App.UiElement.ticket_perform_action
|
|||
if groupAndAttribute
|
||||
elementRow.find('.js-attributeSelector select').val(groupAndAttribute)
|
||||
|
||||
if groupAndAttribute is 'notification.email'
|
||||
notificationTypeMatch = groupAndAttribute.match(/^notification.([\w]+)$/)
|
||||
|
||||
if _.isArray(notificationTypeMatch) && notificationType = notificationTypeMatch[1]
|
||||
elementRow.find('.js-setAttribute').html('')
|
||||
@buildRecipientList(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
@buildRecipientList(notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
else
|
||||
elementRow.find('.js-setNotification').html('')
|
||||
if !elementRow.find('.js-setAttribute div').get(0)
|
||||
|
@ -304,9 +307,11 @@ class App.UiElement.ticket_perform_action
|
|||
|
||||
elementRow.find('.js-value').removeClass('hide').html(item)
|
||||
|
||||
@buildRecipientList: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
@buildRecipientList: (notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
return if elementRow.find('.js-setNotification .js-body').get(0)
|
||||
return if elementRow.find(".js-setNotification .js-body-#{notificationType}").get(0)
|
||||
|
||||
elementRow.find('.js-setNotification').empty()
|
||||
|
||||
options =
|
||||
'article_last_sender': 'Article Last Sender'
|
||||
|
@ -314,7 +319,11 @@ class App.UiElement.ticket_perform_action
|
|||
'ticket_customer': 'Customer'
|
||||
'ticket_agents': 'All Agents'
|
||||
|
||||
name = "#{attribute.name}::notification.email"
|
||||
name = "#{attribute.name}::notification.#{notificationType}"
|
||||
|
||||
messageLength = switch notificationType
|
||||
when 'sms' then 160
|
||||
else 200000
|
||||
|
||||
# meta.recipient was a string in the past (single-select) so we convert it to array if needed
|
||||
if !_.isArray(meta.recipient)
|
||||
|
@ -338,13 +347,14 @@ class App.UiElement.ticket_perform_action
|
|||
notificationElement = $( App.view('generic/ticket_perform_action/notification_email')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
notificationType: notificationType
|
||||
meta: meta || {}
|
||||
))
|
||||
notificationElement.find('.js-recipient select').replaceWith(selection)
|
||||
notificationElement.find('.js-body div[contenteditable="true"]').ce(
|
||||
mode: 'richtext'
|
||||
placeholder: 'message'
|
||||
maxlength: 200000
|
||||
maxlength: messageLength
|
||||
)
|
||||
new App.WidgetPlaceholder(
|
||||
el: notificationElement.find('.js-body div[contenteditable="true"]').parent()
|
||||
|
|
|
@ -380,7 +380,7 @@ class App.TicketCreate extends App.Controller
|
|||
@tokanice()
|
||||
|
||||
tokanice: ->
|
||||
App.Utils.tokaniceEmails('.content.active input[name=cc]')
|
||||
App.Utils.tokanice('.content.active input[name=cc]', 'email')
|
||||
|
||||
localUserInfo: (e) =>
|
||||
return if !@sidebarWidget
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
class SmsReply
|
||||
@action: (actions, ticket, article, ui) ->
|
||||
return actions if ui.permissionCheck('ticket.customer')
|
||||
|
||||
if article.sender.name is 'Customer' && article.type.name is 'sms'
|
||||
actions.push {
|
||||
name: 'reply'
|
||||
type: 'smsMessageReply'
|
||||
icon: 'reply'
|
||||
href: '#'
|
||||
}
|
||||
|
||||
actions
|
||||
|
||||
@perform: (articleContainer, type, ticket, article, ui) ->
|
||||
return true if type isnt 'smsMessageReply'
|
||||
|
||||
ui.scrollToCompose()
|
||||
|
||||
# get reference article
|
||||
type = App.TicketArticleType.find(article.type_id)
|
||||
|
||||
articleNew = {
|
||||
to: article.from
|
||||
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 'sms'
|
||||
articleTypes.push {
|
||||
name: 'sms'
|
||||
icon: 'sms'
|
||||
attributes: ['to']
|
||||
internal: false,
|
||||
features: ['body:limit']
|
||||
maxTextLength: 160
|
||||
warningTextLength: 30
|
||||
}
|
||||
articleTypes
|
||||
|
||||
@setArticleTypePost: (type, ticket, ui) ->
|
||||
return if type isnt 'telegram personal-message'
|
||||
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 'sms'
|
||||
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-SmsReply', SmsReply, 'TicketZoomArticleAction')
|
|
@ -98,7 +98,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
# set expand of text area only once
|
||||
@bind('ui::ticket::shown', (data) =>
|
||||
return if data.ticket_id.toString() isnt @ticket.id.toString()
|
||||
@tokanice()
|
||||
@tokanice(@type)
|
||||
)
|
||||
|
||||
# rerender, e. g. on language change
|
||||
|
@ -106,8 +106,8 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
@render()
|
||||
)
|
||||
|
||||
tokanice: ->
|
||||
App.Utils.tokaniceEmails('.content.active .js-to, .js-cc, js-bcc')
|
||||
tokanice: (type = 'email') ->
|
||||
App.Utils.tokanice('.content.active .js-to, .js-cc, js-bcc', type)
|
||||
|
||||
setPossibleArticleTypes: =>
|
||||
@articleTypes = []
|
||||
|
@ -163,7 +163,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
position: 'right'
|
||||
)
|
||||
|
||||
@tokanice()
|
||||
@tokanice(@type)
|
||||
|
||||
@$('[data-name="body"]').ce({
|
||||
mode: 'richtext'
|
||||
|
@ -346,7 +346,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
@setArticleTypePost(articleTypeToSet)
|
||||
|
||||
$(window).off('click.ticket-zoom-select-type')
|
||||
@tokanice()
|
||||
@tokanice(articleTypeToSet)
|
||||
|
||||
hideSelectableArticleType: =>
|
||||
@el.find('.js-articleTypes').addClass('is-hidden')
|
||||
|
|
|
@ -44,6 +44,7 @@ class App extends Spine.Controller
|
|||
App.Utils.decimal(data, positions)
|
||||
|
||||
# define mask helper
|
||||
# mask an value like 'a***********yz'
|
||||
M: (item, start = 1, end = 2) ->
|
||||
return '' if !item
|
||||
string = ''
|
||||
|
|
|
@ -1121,7 +1121,7 @@ class App.Utils
|
|||
articleNew
|
||||
|
||||
# apply email token field with autocompletion
|
||||
@tokaniceEmails: (selector) ->
|
||||
@tokanice: (selector, type) ->
|
||||
source = "#{App.Config.get('api_path')}/users/search"
|
||||
a = ->
|
||||
$(selector).tokenfield(
|
||||
|
@ -1131,7 +1131,7 @@ class App.Utils
|
|||
minLength: 2
|
||||
},
|
||||
).on('tokenfield:createtoken', (e) ->
|
||||
if !e.attrs.value.match(/@/) || e.attrs.value.match(/\s/)
|
||||
if type is 'email' && !e.attrs.value.match(/@/) || e.attrs.value.match(/\s/)
|
||||
e.preventDefault()
|
||||
return false
|
||||
e.attrs.label = e.attrs.value
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class App.Channel extends App.Model
|
||||
@configure 'Channel', 'adapter', 'area', 'options', 'group_id', 'active', 'updated_at'
|
||||
@configure 'Channel', 'adapter', 'area', 'options', 'group_id', 'active'
|
||||
@extend Spine.Model.Ajax
|
||||
@url: @apiPath + '/channels'
|
||||
|
||||
|
|
11
app/assets/javascripts/app/views/channel/_header.jst.eco
Normal file
11
app/assets/javascripts/app/views/channel/_header.jst.eco
Normal file
|
@ -0,0 +1,11 @@
|
|||
<div class="page-header">
|
||||
<div class="page-header-title">
|
||||
<% if @params.hasSwitch: %>
|
||||
<div class="zammad-switch zammad-switch--small js-channelActive">
|
||||
<input name="sms_channel_active" type="checkbox" id="sms_channel_active">
|
||||
<label for="sms_channel_active"></label>
|
||||
</div>
|
||||
<% end %>
|
||||
<h1><%- @T(@params.header) %> <small></small></h1>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,130 @@
|
|||
<h2><%- @T('SMS Accounts') %></h2>
|
||||
|
||||
<% if _.isEmpty(@account_channels): %>
|
||||
<p><%- @T('You have no configured account right now.') %></p>
|
||||
<% else: %>
|
||||
<% for channel in @account_channels: %>
|
||||
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%- channel.id %>">
|
||||
<div class="action-flow" style="width: 100%;">
|
||||
<div class="action-block action-block--flex">
|
||||
<div class="horizontal">
|
||||
<h3><%- @Icon('status', channel.status_in + " inline") %> <%- @T('Inbound') %></h3>
|
||||
</div>
|
||||
<% if !_.isEmpty(channel.last_log_in): %>
|
||||
<div class="alert alert--danger">
|
||||
<%= channel.last_log_in %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="action-block action-block--flex">
|
||||
<div class="horizontal">
|
||||
<h3><%- @Icon('status', channel.status_out + " inline") %> <%- @T('Outbound') %></h3>
|
||||
</div>
|
||||
<% if !_.isEmpty(channel.last_log_out): %>
|
||||
<div class="alert alert--danger">
|
||||
<%= channel.last_log_out %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-flow" style="width: 100%;">
|
||||
<div class="action-block action-block--flex">
|
||||
<table class="key-value">
|
||||
<tr>
|
||||
<td><%- @T('Adapter') %>
|
||||
<td><%= channel.options.adapter %>
|
||||
<tr>
|
||||
<td><%- @T('Webhook') %>
|
||||
<td><%= @C('http_type') %>://<%= @C('fqdn') %>/api/v1/sms_webhook/<%= channel.options?.webhook_token || '?' %>
|
||||
<tr>
|
||||
<td><%- @T('Account') %>
|
||||
<td><%= channel.options.account_id %>
|
||||
<tr>
|
||||
<td><%- @T('Token') %>
|
||||
<td><%= @M(channel.options.token, 1, 2) %>
|
||||
<tr>
|
||||
<td><%- @T('Sender') %>
|
||||
<td><%= channel.options.sender %>
|
||||
<tr>
|
||||
<td><%- @T('Group') %>
|
||||
<td>
|
||||
<div href="#" class="js-channelEdit <% if channel.group.active is false: %>alert alert--danger<% end %>">
|
||||
<% groupName = '' %>
|
||||
<% if channel.group.displayName: %>
|
||||
<% groupName = channel.group.displayName() %>
|
||||
<% else: %>
|
||||
<% groupName = channel.group %>
|
||||
<% end %>
|
||||
<% if channel.group.active is false: %>
|
||||
<%- @T('%s is inactive, please select a active one.', groupName) %>
|
||||
<% else: %>
|
||||
<%= groupName %>
|
||||
<% end %>
|
||||
</div>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-controls">
|
||||
<div class="btn btn--secondary js-channelEdit"><%- @T('Edit') %></div>
|
||||
<% if channel.active is true: %>
|
||||
<div class="btn btn--secondary js-channelDisable"><%- @T('Disable') %></div>
|
||||
<% else: %>
|
||||
<div class="btn btn--secondary js-channelEnable"><%- @T('Enable') %></div>
|
||||
<% end %>
|
||||
<div class="btn btn--danger btn--secondary js-channelDelete"><%- @T('Delete') %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<a class="btn btn--success js-channelEdit"><%- @T('New') %></a>
|
||||
|
||||
<h2><%- @T('SMS Notification') %></h2>
|
||||
<% if _.isEmpty(@notification_channels): %>
|
||||
<p><%- @T('You have no configured account right now.') %></p>
|
||||
<a class="btn btn--success js-editNotification"><%- @T('New') %></a>
|
||||
<% else: %>
|
||||
<% for channel in @notification_channels: %>
|
||||
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%- channel.id %>">
|
||||
<div class="action-flow action-flow--row">
|
||||
<div class="action-block action-block--flex">
|
||||
<div class="horizontal">
|
||||
<h3><%- @Icon('status', channel.status_out + " inline") %> <%- @T('Outbound') %></h3>
|
||||
</div>
|
||||
<% if channel.status_in is 'error': %>
|
||||
<div class="alert alert--danger"><%= channel.last_log_in %></div>
|
||||
<% end %>
|
||||
<% if channel.status_out is 'error': %>
|
||||
<div class="alert alert--danger"><%= channel.last_log_out %></div>
|
||||
<% end %>
|
||||
<table class="key-value">
|
||||
<% if channel.options: %>
|
||||
<tr>
|
||||
<td><%- @T('Adapter') %>
|
||||
<td><%= channel.options.adapter %>
|
||||
<tr>
|
||||
<td><%- @T('Token') %>
|
||||
<td><%= @M(channel.options.token, 1, 2) %>
|
||||
<% end %>
|
||||
<tr>
|
||||
<td><%- @T('Sender') %>
|
||||
<td><%= channel.options.sender %>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-controls">
|
||||
<div class="btn btn--secondary js-editNotification"><%- @T('Edit') %></div>
|
||||
<% if channel.active is true: %>
|
||||
<div class="btn btn--secondary js-channelDisable"><%- @T('Disable') %></div>
|
||||
<% else: %>
|
||||
<div class="btn btn--secondary js-channelEnable"><%- @T('Enable') %></div>
|
||||
<% end %>
|
||||
<div class="btn btn--danger btn--secondary js-channelDelete"><%- @T('Delete') %></div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -3,6 +3,8 @@
|
|||
<select></select>
|
||||
<%- @Icon('arrow-down', 'dropdown-arrow') %>
|
||||
</div>
|
||||
<div class="controls js-subject"><input type="text" name="<%= @name %>::subject" value="<%= @meta.subject %>" style="width: 100%;" placeholder="<%- @T('Subject') %>"></div>
|
||||
<div class="controls js-body"><div class="richtext form-control"><div contenteditable="true" data-name="<%= @name %>::body"><%- @meta.body %></div></div></div>
|
||||
<% if @notificationType is 'email': %>
|
||||
<div class="controls js-subject"><input type="text" name="<%= @name %>::subject" value="<%= @meta.subject %>" style="width: 100%;" placeholder="<%- @T('Subject') %>"></div>
|
||||
<% end %>
|
||||
<div class="controls js-body js-body-<%- @notificationType %>"><div class="richtext form-control"><div contenteditable="true" data-name="<%= @name %>::body"><%- @meta.body %></div></div></div>
|
||||
</div>
|
||||
|
|
|
@ -55,7 +55,9 @@
|
|||
.icon-lock { width: 16px; height: 17px; }
|
||||
.icon-logo { width: 42px; height: 36px; }
|
||||
.icon-logotype { width: 91px; height: 15px; }
|
||||
.icon-long-arrow-down { width: 11px; height: 11px; }
|
||||
.icon-long-arrow-right { width: 11px; height: 11px; }
|
||||
.icon-low-priority { width: 16px; height: 16px; }
|
||||
.icon-magnifier { width: 15px; height: 15px; }
|
||||
.icon-marker { width: 17px; height: 19px; }
|
||||
.icon-message { width: 24px; height: 24px; }
|
||||
|
@ -94,6 +96,7 @@
|
|||
.icon-searchdetail { width: 18px; height: 14px; }
|
||||
.icon-signout { width: 15px; height: 19px; }
|
||||
.icon-small-dot { width: 16px; height: 16px; }
|
||||
.icon-sms { width: 17px; height: 17px; }
|
||||
.icon-split { width: 16px; height: 17px; }
|
||||
.icon-status-modified-outer-circle { width: 16px; height: 16px; }
|
||||
.icon-status { width: 16px; height: 16px; }
|
||||
|
|
|
@ -2240,6 +2240,10 @@ kbd {
|
|||
.help-block {
|
||||
color: hsl(198,19%,72%);
|
||||
}
|
||||
.content-controls-align-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.page-description p {
|
||||
|
@ -7790,6 +7794,10 @@ output {
|
|||
border-color: hsl(210,10%,85%);
|
||||
}
|
||||
|
||||
&[disabled] + label:after {
|
||||
background: hsl(210,17%,97%);
|
||||
}
|
||||
|
||||
&:focus + label {
|
||||
transition: none;
|
||||
background: hsl(200,71%,59%);
|
||||
|
|
47
app/controllers/application_channel_controller.rb
Normal file
47
app/controllers/application_channel_controller.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
class ApplicationChannelController < ApplicationController
|
||||
# Extending controllers has to define following constants:
|
||||
# PERMISSION = "admin.channel_xyz"
|
||||
# AREA = "XYZ::Account"
|
||||
|
||||
def index
|
||||
render json: channels_data
|
||||
end
|
||||
|
||||
def show
|
||||
model_show_render(Channel, params)
|
||||
end
|
||||
|
||||
def create
|
||||
model_create_render(Channel, channel_params)
|
||||
end
|
||||
|
||||
def update
|
||||
model_update_render(Channel, channel_params)
|
||||
end
|
||||
|
||||
def enable
|
||||
channel.update!(active: true)
|
||||
render json: channel
|
||||
end
|
||||
|
||||
def disable
|
||||
channel.update!(active: false)
|
||||
render json: channel
|
||||
end
|
||||
|
||||
def destroy
|
||||
channel.destroy!
|
||||
render json: {}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def channel
|
||||
@channel ||= Channel.lookup(id: params[:id])
|
||||
end
|
||||
|
||||
def channel_params
|
||||
params = params.permit!.to_s
|
||||
end
|
||||
|
||||
end
|
86
app/controllers/channels_sms_controller.rb
Normal file
86
app/controllers/channels_sms_controller.rb
Normal file
|
@ -0,0 +1,86 @@
|
|||
class ChannelsSmsController < ApplicationChannelController
|
||||
PERMISSION = 'admin.channel_sms'.freeze
|
||||
AREA = ['Sms::Notification'.freeze, 'Sms::Account'.freeze].freeze
|
||||
|
||||
prepend_before_action -> { authentication_check(permission: self.class::PERMISSION) }, except: [:webhook]
|
||||
skip_before_action :verify_csrf_token, only: [:webhook]
|
||||
|
||||
def index
|
||||
assets = {}
|
||||
render json: {
|
||||
account_channel_ids: channels_data('Sms::Account', assets),
|
||||
notification_channel_ids: channels_data('Sms::Notification', assets),
|
||||
config: channels_config,
|
||||
assets: assets
|
||||
}
|
||||
end
|
||||
|
||||
def test
|
||||
raise 'Missing parameter options.adapter' if params[:options][:adapter].blank?
|
||||
|
||||
driver = Channel.driver_class(params[:options][:adapter])
|
||||
resp = driver.new.send(params[:options], test_options)
|
||||
|
||||
render json: { success: resp }
|
||||
rescue => e
|
||||
render json: { error: e.inspect, error_human: e.message }
|
||||
end
|
||||
|
||||
def webhook
|
||||
raise Exceptions::UnprocessableEntity, 'token param missing' if params['token'].blank?
|
||||
|
||||
channel = nil
|
||||
Channel.where(active: true, area: 'Sms::Account').each do |local_channel|
|
||||
next if local_channel.options[:webhook_token] != params['token']
|
||||
|
||||
channel = local_channel
|
||||
end
|
||||
if !channel
|
||||
render(
|
||||
json: { message: 'channel not found' },
|
||||
status: :not_found
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
conten_type, content = channel.process(params.permit!.to_h)
|
||||
send_data content, type: conten_type
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def test_options
|
||||
params.permit(:recipient, :message)
|
||||
end
|
||||
|
||||
def channel_params
|
||||
raise 'Missing area params' if params[:area].blank?
|
||||
if !self.class::AREA.include?(params[:area])
|
||||
raise "Invalid area '#{params[:area]}'!"
|
||||
end
|
||||
raise 'Missing options params' if params[:options].blank?
|
||||
raise 'Missing options.adapter params' if params[:options][:adapter].blank?
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def channels_config
|
||||
list = []
|
||||
Dir.glob(Rails.root.join('app', 'models', 'channel', 'driver', 'sms', '*.rb')).each do |path|
|
||||
filename = File.basename(path)
|
||||
require_dependency "channel/driver/sms/#{filename.sub('.rb', '')}"
|
||||
list.push Channel.driver_class("sms/#{filename}").definition
|
||||
end
|
||||
list
|
||||
end
|
||||
|
||||
def channels_data(area, assets)
|
||||
channel_ids = []
|
||||
Channel.where(area: area).each do |channel|
|
||||
assets = channel.assets(assets)
|
||||
channel_ids.push(channel.id)
|
||||
end
|
||||
channel_ids
|
||||
end
|
||||
|
||||
end
|
|
@ -216,6 +216,7 @@ curl http://localhost/api/v1/monitoring/status?token=XXX
|
|||
overviews: Overview,
|
||||
tickets: Ticket,
|
||||
ticket_articles: Ticket::Article,
|
||||
text_modules: TextModule,
|
||||
}
|
||||
map.each do |key, class_name|
|
||||
status[:counts][key] = class_name.count
|
||||
|
|
|
@ -49,13 +49,7 @@ fetch one account
|
|||
end
|
||||
|
||||
begin
|
||||
# we need to require each channel backend individually otherwise we get a
|
||||
# 'warning: toplevel constant Twitter referenced by Channel::Driver::Twitter' error e.g.
|
||||
# so we have to convert the channel name to the filename via Rails String.underscore
|
||||
# http://stem.ps/rails/2015/01/25/ruby-gotcha-toplevel-constant-referenced-by.html
|
||||
require "channel/driver/#{adapter.to_filename}"
|
||||
|
||||
driver_class = Object.const_get("Channel::Driver::#{adapter.to_classname}")
|
||||
driver_class = self.class.driver_class(adapter)
|
||||
driver_instance = driver_class.new
|
||||
return if !force && !driver_instance.fetchable?(self)
|
||||
|
||||
|
@ -93,13 +87,7 @@ stream instance of account
|
|||
adapter = options[:adapter]
|
||||
|
||||
begin
|
||||
# we need to require each channel backend individually otherwise we get a
|
||||
# 'warning: toplevel constant Twitter referenced by Channel::Driver::Twitter' error e.g.
|
||||
# so we have to convert the channel name to the filename via Rails String.underscore
|
||||
# http://stem.ps/rails/2015/01/25/ruby-gotcha-toplevel-constant-referenced-by.html
|
||||
require "channel/driver/#{adapter.to_filename}"
|
||||
|
||||
driver_class = Object.const_get("Channel::Driver::#{adapter.to_classname}")
|
||||
driver_class = self.class.driver_class(adapter)
|
||||
driver_instance = driver_class.new
|
||||
|
||||
# check is stream exists
|
||||
|
@ -145,7 +133,7 @@ stream all accounts
|
|||
adapter = channel.options[:adapter]
|
||||
next if adapter.blank?
|
||||
|
||||
driver_class = Object.const_get("Channel::Driver::#{adapter.to_classname}")
|
||||
driver_class = self.driver_class(adapter)
|
||||
next if !driver_class.respond_to?(:streamable?)
|
||||
next if !driver_class.streamable?
|
||||
|
||||
|
@ -250,31 +238,22 @@ stream all accounts
|
|||
send via account
|
||||
|
||||
channel = Channel.where(area: 'Email::Account').first
|
||||
channel.deliver(mail_params, notification)
|
||||
channel.deliver(params, notification)
|
||||
|
||||
=end
|
||||
|
||||
def deliver(mail_params, notification = false)
|
||||
|
||||
def deliver(params, notification = false)
|
||||
adapter = options[:adapter]
|
||||
adapter_options = options
|
||||
if options[:outbound] && options[:outbound][:adapter]
|
||||
adapter = options[:outbound][:adapter]
|
||||
adapter_options = options[:outbound][:options]
|
||||
end
|
||||
|
||||
result = nil
|
||||
|
||||
begin
|
||||
# we need to require each channel backend individually otherwise we get a
|
||||
# 'warning: toplevel constant Twitter referenced by Channel::Driver::Twitter' error e.g.
|
||||
# so we have to convert the channel name to the filename via Rails String.underscore
|
||||
# http://stem.ps/rails/2015/01/25/ruby-gotcha-toplevel-constant-referenced-by.html
|
||||
require "channel/driver/#{adapter.to_filename}"
|
||||
|
||||
driver_class = Object.const_get("Channel::Driver::#{adapter.to_classname}")
|
||||
driver_class = self.class.driver_class(adapter)
|
||||
driver_instance = driver_class.new
|
||||
result = driver_instance.send(adapter_options, mail_params, notification)
|
||||
result = driver_instance.send(adapter_options, params, notification)
|
||||
self.status_out = 'ok'
|
||||
self.last_log_out = ''
|
||||
save!
|
||||
|
@ -290,6 +269,72 @@ send via account
|
|||
result
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
process via account
|
||||
|
||||
channel = Channel.where(area: 'Email::Account').first
|
||||
channel.process(params)
|
||||
|
||||
=end
|
||||
|
||||
def process(params)
|
||||
adapter = options[:adapter]
|
||||
adapter_options = options
|
||||
if options[:inbound] && options[:inbound][:adapter]
|
||||
adapter = options[:inbound][:adapter]
|
||||
adapter_options = options[:inbound][:options]
|
||||
end
|
||||
result = nil
|
||||
begin
|
||||
driver_class = self.class.driver_class(adapter)
|
||||
driver_instance = driver_class.new
|
||||
result = driver_instance.process(adapter_options, params, self)
|
||||
self.status_in = 'ok'
|
||||
self.last_log_in = ''
|
||||
save!
|
||||
rescue => e
|
||||
error = "Can't use Channel::Driver::#{adapter.to_classname}: #{e.inspect}"
|
||||
logger.error error
|
||||
logger.error e.backtrace
|
||||
self.status_in = 'error'
|
||||
self.last_log_in = error
|
||||
save!
|
||||
raise e, error
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
load channel driver and return class
|
||||
|
||||
klass = Channel.driver_class('Imap')
|
||||
|
||||
=end
|
||||
|
||||
def self.driver_class(adapter)
|
||||
# we need to require each channel backend individually otherwise we get a
|
||||
# 'warning: toplevel constant Twitter referenced by Channel::Driver::Twitter' error e.g.
|
||||
# so we have to convert the channel name to the filename via Rails String.underscore
|
||||
# http://stem.ps/rails/2015/01/25/ruby-gotcha-toplevel-constant-referenced-by.html
|
||||
require "channel/driver/#{adapter.to_filename}"
|
||||
|
||||
Object.const_get("::Channel::Driver::#{adapter.to_classname}")
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
get instance of channel driver
|
||||
|
||||
channel.driver_instance
|
||||
|
||||
=end
|
||||
|
||||
def driver_instance
|
||||
self.class.driver_class(options[:adapter])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def email_address_check
|
||||
|
|
51
app/models/channel/driver/sms/massenversand.rb
Normal file
51
app/models/channel/driver/sms/massenversand.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
class Channel::Driver::Sms::Massenversand
|
||||
NAME = 'sms/massenversand'.freeze
|
||||
|
||||
def send(options, attr, _notification = false)
|
||||
Rails.logger.info "Sending SMS to recipient #{attr[:recipient]}"
|
||||
|
||||
return true if Setting.get('import_mode')
|
||||
|
||||
Rails.logger.info "Backend sending Massenversand SMS to #{attr[:recipient]}"
|
||||
begin
|
||||
url = build_url(options, attr)
|
||||
|
||||
if Setting.get('developer_mode') != true
|
||||
response = Faraday.get(url).body
|
||||
raise response if !response.match?('OK')
|
||||
end
|
||||
|
||||
true
|
||||
rescue => e
|
||||
Rails.logger.debug "Massenversand error: #{e.inspect}"
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
def self.definition
|
||||
{
|
||||
name: 'Massenversand',
|
||||
adapter: 'sms/massenversand',
|
||||
notification: [
|
||||
{ name: 'options::gateway', display: 'Gateway', tag: 'input', type: 'text', limit: 200, null: false, placeholder: 'https://gate1.goyyamobile.com/sms/sendsms.asp', default: 'https://gate1.goyyamobile.com/sms/sendsms.asp' },
|
||||
{ name: 'options::token', display: 'Token', tag: 'input', type: 'text', limit: 200, null: false, placeholder: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' },
|
||||
{ name: 'options::sender', display: 'Sender', tag: 'input', type: 'text', limit: 200, null: false, placeholder: '00491710000000' },
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_url(options, attr)
|
||||
params = {
|
||||
authToken: options[:token],
|
||||
getID: 1,
|
||||
msg: attr[:message],
|
||||
msgtype: 'c',
|
||||
receiver: attr[:recipient],
|
||||
sender: options[:sender]
|
||||
}
|
||||
|
||||
options[:gateway] + '?' + URI.encode_www_form(params)
|
||||
end
|
||||
end
|
147
app/models/channel/driver/sms/twilio.rb
Normal file
147
app/models/channel/driver/sms/twilio.rb
Normal file
|
@ -0,0 +1,147 @@
|
|||
class Channel::Driver::Sms::Twilio
|
||||
NAME = 'sms/twilio'.freeze
|
||||
|
||||
def fetchable?(_channel)
|
||||
false
|
||||
end
|
||||
|
||||
def send(options, attr, _notification = false)
|
||||
Rails.logger.info "Sending SMS to recipient #{attr[:recipient]}"
|
||||
|
||||
return true if Setting.get('import_mode')
|
||||
|
||||
Rails.logger.info "Backend sending Twilio SMS to #{attr[:recipient]}"
|
||||
begin
|
||||
if Setting.get('developer_mode') != true
|
||||
result = api(options).messages.create(
|
||||
from: options[:sender],
|
||||
to: attr[:recipient],
|
||||
body: attr[:message],
|
||||
)
|
||||
|
||||
raise result.error_message if result.error_code.positive?
|
||||
end
|
||||
|
||||
true
|
||||
rescue => e
|
||||
Rails.logger.debug "Twilio error: #{e.inspect}"
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
def process(_options, attr, channel)
|
||||
Rails.logger.info "Receiving SMS frim recipient #{attr[:From]}"
|
||||
|
||||
# prevent already created articles
|
||||
if Ticket::Article.find_by(message_id: attr[:SmsMessageSid])
|
||||
return ['application/xml; charset=UTF-8;', Twilio::TwiML::MessagingResponse.new.to_s]
|
||||
end
|
||||
|
||||
# find sender
|
||||
user = User.where(mobile: attr[:From]).order(:updated_at).first
|
||||
if !user
|
||||
from_comment, preferences = Cti::CallerId.get_comment_preferences(attr[:From], 'from')
|
||||
if preferences && preferences['from'] && preferences['from'][0]
|
||||
if preferences['from'][0]['level'] == 'known' && preferences['from'][0]['object'] == 'User'
|
||||
user = User.find_by(id: preferences['from'][0]['o_id'])
|
||||
end
|
||||
end
|
||||
end
|
||||
if !user
|
||||
user = User.create!(
|
||||
firstname: attr[:From],
|
||||
mobile: attr[:From],
|
||||
)
|
||||
end
|
||||
|
||||
UserInfo.current_user_id = user.id
|
||||
|
||||
# find ticket
|
||||
article_type_sms = Ticket::Article::Type.find_by(name: 'sms')
|
||||
state_ids = Ticket::State.where(name: %w[closed merged removed]).pluck(:id)
|
||||
ticket = Ticket.where(customer_id: user.id, create_article_type_id: article_type_sms.id).where.not(state_id: state_ids).order(:updated_at).first
|
||||
if ticket
|
||||
new_state = Ticket::State.find_by(default_create: true)
|
||||
if ticket.state_id != new_state.id
|
||||
ticket.state = Ticket::State.find_by(default_follow_up: true)
|
||||
ticket.save!
|
||||
end
|
||||
else
|
||||
if channel.group_id.blank?
|
||||
raise Exceptions::UnprocessableEntity, 'Group needed in channel definition!'
|
||||
end
|
||||
|
||||
group = Group.find_by(id: channel.group_id)
|
||||
if !group
|
||||
raise Exceptions::UnprocessableEntity, 'Group is invalid!'
|
||||
end
|
||||
|
||||
title = attr[:Body]
|
||||
if title.length > 40
|
||||
title = "#{title[0, 40]}..."
|
||||
end
|
||||
ticket = Ticket.new(
|
||||
group_id: channel.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,
|
||||
sms: {
|
||||
AccountSid: attr['AccountSid'],
|
||||
From: attr['From'],
|
||||
To: attr['To'],
|
||||
}
|
||||
}
|
||||
)
|
||||
ticket.save!
|
||||
end
|
||||
|
||||
Ticket::Article.create!(
|
||||
ticket_id: ticket.id,
|
||||
type: article_type_sms,
|
||||
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
|
||||
body: attr[:Body],
|
||||
from: attr[:From],
|
||||
to: attr[:To],
|
||||
message_id: attr[:SmsMessageSid],
|
||||
content_type: 'text/plain',
|
||||
preferences: {
|
||||
channel_id: channel.id,
|
||||
sms: {
|
||||
AccountSid: attr['AccountSid'],
|
||||
From: attr['From'],
|
||||
To: attr['To'],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
['application/xml; charset=UTF-8;', Twilio::TwiML::MessagingResponse.new.to_s]
|
||||
end
|
||||
|
||||
def self.definition
|
||||
{
|
||||
name: 'twilio',
|
||||
adapter: 'sms/twilio',
|
||||
account: [
|
||||
{ name: 'options::webhook_token', display: 'Webhook Token', tag: 'input', type: 'text', limit: 200, null: false, default: Digest::MD5.hexdigest(rand(999_999_999_999).to_s), disabled: true, readonly: true },
|
||||
{ name: 'options::account_id', display: 'Account SID', tag: 'input', type: 'text', limit: 200, null: false, placeholder: 'XXXXXX' },
|
||||
{ name: 'options::token', display: 'Token', tag: 'input', type: 'text', limit: 200, null: false },
|
||||
{ name: 'options::sender', display: 'Sender', tag: 'input', type: 'text', limit: 200, null: false, placeholder: '+491710000000' },
|
||||
{ name: 'group_id', display: 'Destination Group', tag: 'select', null: false, relation: 'Group', nulloption: true, filter: { active: true } },
|
||||
],
|
||||
notification: [
|
||||
{ name: 'options::account_id', display: 'Account SID', tag: 'input', type: 'text', limit: 200, null: false, placeholder: 'XXXXXX' },
|
||||
{ name: 'options::token', display: 'Token', tag: 'input', type: 'text', limit: 200, null: false },
|
||||
{ name: 'options::sender', display: 'Sender', tag: 'input', type: 'text', limit: 200, null: false, placeholder: '+491710000000' },
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def api(options)
|
||||
@api ||= ::Twilio::REST::Client.new options[:account_id], options[:token]
|
||||
end
|
||||
end
|
|
@ -243,6 +243,34 @@ returns
|
|||
end
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
from_comment, preferences = Cti::CallerId.get_comment_preferences('00491710000000', 'from')
|
||||
|
||||
returns
|
||||
|
||||
[
|
||||
"Bob Smith",
|
||||
{
|
||||
"from"=>[
|
||||
{
|
||||
"id"=>1961634,
|
||||
"caller_id"=>"491710000000",
|
||||
"comment"=>nil,
|
||||
"level"=>"known",
|
||||
"object"=>"User",
|
||||
"o_id"=>3,
|
||||
"user_id"=>3,
|
||||
"preferences"=>nil,
|
||||
"created_at"=>Mon, 24 Sep 2018 15:19:48 UTC +00:00,
|
||||
"updated_at"=>Mon, 24 Sep 2018 15:19:48 UTC +00:00,
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
=end
|
||||
|
||||
def self.get_comment_preferences(caller_id, direction)
|
||||
from_comment_known = ''
|
||||
from_comment_maybe = ''
|
||||
|
|
|
@ -6,24 +6,25 @@ class Observer::Ticket::Article::CommunicateEmail < ActiveRecord::Observer
|
|||
def after_create(record)
|
||||
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
return true if Setting.get('import_mode')
|
||||
|
||||
# only do send email if article got created via application_server (e. g. not
|
||||
# if article and sender type is set via *.postmaster)
|
||||
return if ApplicationHandleInfo.postmaster?
|
||||
return true if ApplicationHandleInfo.postmaster?
|
||||
|
||||
# if sender is customer, do not communicate
|
||||
return if !record.sender_id
|
||||
return true if !record.sender_id
|
||||
|
||||
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
|
||||
return 1 if sender.nil?
|
||||
return 1 if sender['name'] == 'Customer'
|
||||
return true if sender.nil?
|
||||
return true if sender.name == 'Customer'
|
||||
|
||||
# only apply on emails
|
||||
return if !record.type_id
|
||||
return true if !record.type_id
|
||||
|
||||
type = Ticket::Article::Type.lookup(id: record.type_id)
|
||||
return if type['name'] != 'email'
|
||||
return true if type.nil?
|
||||
return true if type.name != 'email'
|
||||
|
||||
# send background job
|
||||
Delayed::Job.enqueue(Observer::Ticket::Article::CommunicateEmail::BackgroundJob.new(record.id))
|
||||
|
|
|
@ -15,9 +15,7 @@ class Observer::Ticket::Article::CommunicateEmail::BackgroundJob
|
|||
subject = ticket.subject_build(record.subject, subject_prefix_mode)
|
||||
|
||||
# set retry count
|
||||
if !record.preferences['delivery_retry']
|
||||
record.preferences['delivery_retry'] = 0
|
||||
end
|
||||
record.preferences['delivery_retry'] ||= 0
|
||||
record.preferences['delivery_retry'] += 1
|
||||
|
||||
# send email
|
||||
|
|
|
@ -5,24 +5,25 @@ class Observer::Ticket::Article::CommunicateFacebook < ActiveRecord::Observer
|
|||
def after_create(record)
|
||||
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
return true if Setting.get('import_mode')
|
||||
|
||||
# only do send email if article got created via application_server (e. g. not
|
||||
# if article and sender type is set via *.postmaster)
|
||||
return if ApplicationHandleInfo.postmaster?
|
||||
return true if ApplicationHandleInfo.postmaster?
|
||||
|
||||
# if sender is customer, do not communicate
|
||||
return if !record.sender_id
|
||||
return true if !record.sender_id
|
||||
|
||||
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
|
||||
return 1 if sender.nil?
|
||||
return 1 if sender['name'] == 'Customer'
|
||||
return true if sender.nil?
|
||||
return true if sender.name == 'Customer'
|
||||
|
||||
# only apply for facebook
|
||||
return if !record.type_id
|
||||
return true if !record.type_id
|
||||
|
||||
type = Ticket::Article::Type.lookup(id: record.type_id)
|
||||
return if type['name'] !~ /\Afacebook/
|
||||
return true if type.nil?
|
||||
return true if type.name !~ /\Afacebook/
|
||||
|
||||
Delayed::Job.enqueue(Observer::Ticket::Article::CommunicateFacebook::BackgroundJob.new(record.id))
|
||||
end
|
||||
|
|
25
app/models/observer/ticket/article/communicate_sms.rb
Normal file
25
app/models/observer/ticket/article/communicate_sms.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
class Observer::Ticket::Article::CommunicateSms < 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 if !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 sms
|
||||
return true if !record.type_id
|
||||
|
||||
type = Ticket::Article::Type.lookup(id: record.type_id)
|
||||
return true if type.nil?
|
||||
return true if type.name != 'sms'
|
||||
|
||||
Delayed::Job.enqueue(Observer::Ticket::Article::CommunicateSms::BackgroundJob.new(record.id))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,121 @@
|
|||
class Observer::Ticket::Article::CommunicateSms::BackgroundJob
|
||||
def initialize(id)
|
||||
@article_id = id
|
||||
end
|
||||
|
||||
def perform
|
||||
article = Ticket::Article.find(@article_id)
|
||||
|
||||
article.preferences['delivery_retry'] ||= 0
|
||||
article.preferences['delivery_retry'] += 1
|
||||
|
||||
ticket = Ticket.lookup(id: article.ticket_id)
|
||||
log_error(article, "Can't find article.preferences for Ticket::Article.find(#{article.id})") if !article.preferences
|
||||
|
||||
# if sender is system, take artile channel
|
||||
if article.sender.name == 'System'
|
||||
log_error(article, "Can't find article.preferences['sms_recipients'] for Ticket::Article.find(#{article.id})") if !article.preferences['sms_recipients']
|
||||
log_error(article, "Can't find article.preferences['channel_id'] for Ticket::Article.find(#{article.id})") if !article.preferences['channel_id']
|
||||
channel = Channel.lookup(id: article.preferences['channel_id'])
|
||||
log_error(article, "No such channel id #{article.preferences['channel_id']}") if !channel
|
||||
|
||||
# if sender is agent, take create channel
|
||||
else
|
||||
log_error(article, "Can't find ticket.preferences['channel_id'] for Ticket.find(#{ticket.id})") if !ticket.preferences['channel_id']
|
||||
channel = Channel.lookup(id: ticket.preferences['channel_id'])
|
||||
log_error(article, "No such channel id #{ticket.preferences['channel_id']}") if !channel
|
||||
end
|
||||
|
||||
begin
|
||||
if article.sender.name == 'System'
|
||||
article.preferences['sms_recipients'].each do |recipient|
|
||||
channel.deliver(
|
||||
recipient: recipient,
|
||||
message: article.body.first(160),
|
||||
)
|
||||
end
|
||||
else
|
||||
channel.deliver(
|
||||
recipient: article.to,
|
||||
message: article.body.first(160),
|
||||
)
|
||||
end
|
||||
rescue => e
|
||||
log_error(article, e.message)
|
||||
return
|
||||
end
|
||||
|
||||
log_success(article)
|
||||
|
||||
return if article.sender.name == 'Agent'
|
||||
|
||||
log_history(article, ticket, 'sms', article.to)
|
||||
end
|
||||
|
||||
# log successful delivery
|
||||
def log_success(article)
|
||||
article.preferences['delivery_status_message'] = nil
|
||||
article.preferences['delivery_status'] = 'success'
|
||||
article.preferences['delivery_status_date'] = Time.zone.now
|
||||
article.save!
|
||||
end
|
||||
|
||||
def log_error(local_record, message)
|
||||
local_record.preferences['delivery_status'] = 'fail'
|
||||
local_record.preferences['delivery_status_message'] = message
|
||||
local_record.preferences['delivery_status_date'] = Time.zone.now
|
||||
local_record.save!
|
||||
Rails.logger.error message
|
||||
|
||||
if local_record.preferences['delivery_retry'] >= max_attempts
|
||||
Ticket::Article.create(
|
||||
ticket_id: local_record.ticket_id,
|
||||
content_type: 'text/plain',
|
||||
body: "#{log_error_prefix}: #{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 log_history(article, ticket, history_type, recipient_list)
|
||||
return if recipient_list.blank?
|
||||
|
||||
History.add(
|
||||
o_id: article.id,
|
||||
history_type: history_type,
|
||||
history_object: 'Ticket::Article',
|
||||
related_o_id: ticket.id,
|
||||
related_history_object: 'Ticket',
|
||||
value_from: article.subject,
|
||||
value_to: recipient_list,
|
||||
created_by_id: article.created_by_id,
|
||||
)
|
||||
end
|
||||
|
||||
def log_error_prefix
|
||||
'Unable to send sms message'
|
||||
end
|
||||
|
||||
def max_attempts
|
||||
4
|
||||
end
|
||||
|
||||
def reschedule_at(current_time, attempts)
|
||||
if Rails.env.production?
|
||||
return current_time + attempts * 120.seconds
|
||||
end
|
||||
|
||||
current_time + 5.seconds
|
||||
end
|
||||
|
||||
end
|
|
@ -6,20 +6,20 @@ class Observer::Ticket::Article::CommunicateTelegram < ActiveRecord::Observer
|
|||
def after_create(record)
|
||||
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
return true if Setting.get('import_mode')
|
||||
|
||||
# if sender is customer, do not communicate
|
||||
return if !record.sender_id
|
||||
return true if !record.sender_id
|
||||
|
||||
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
|
||||
return if sender.nil?
|
||||
return if sender['name'] == 'Customer'
|
||||
return true if sender.nil?
|
||||
return true if sender.name == 'Customer'
|
||||
|
||||
# only apply on telegram messages
|
||||
return if !record.type_id
|
||||
return true if !record.type_id
|
||||
|
||||
type = Ticket::Article::Type.lookup(id: record.type_id)
|
||||
return if type['name'] !~ /\Atelegram/i
|
||||
return true if type.name !~ /\Atelegram/i
|
||||
|
||||
Delayed::Job.enqueue(Observer::Ticket::Article::CommunicateTelegram::BackgroundJob.new(record.id))
|
||||
end
|
||||
|
|
|
@ -7,10 +7,8 @@ class Observer::Ticket::Article::CommunicateTelegram::BackgroundJob
|
|||
article = Ticket::Article.find(@article_id)
|
||||
|
||||
# set retry count
|
||||
if !article.preferences['delivery_retry']
|
||||
article.preferences['delivery_retry'] = 0
|
||||
end
|
||||
article.preferences['delivery_retry'] += 1
|
||||
record.preferences['delivery_retry'] ||= 0
|
||||
record.preferences['delivery_retry'] += 1
|
||||
|
||||
ticket = Ticket.lookup(id: article.ticket_id)
|
||||
log_error(article, "Can't find ticket.preferences for Ticket.find(#{article.ticket_id})") if !ticket.preferences
|
||||
|
|
|
@ -6,24 +6,25 @@ class Observer::Ticket::Article::CommunicateTwitter < ActiveRecord::Observer
|
|||
def after_create(record)
|
||||
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
return true if Setting.get('import_mode')
|
||||
|
||||
# only do send email if article got created via application_server (e. g. not
|
||||
# if article and sender type is set via *.postmaster)
|
||||
return if ApplicationHandleInfo.postmaster?
|
||||
return true if ApplicationHandleInfo.postmaster?
|
||||
|
||||
# if sender is customer, do not communicate
|
||||
return if !record.sender_id
|
||||
return true if !record.sender_id
|
||||
|
||||
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
|
||||
return if sender.nil?
|
||||
return if sender['name'] == 'Customer'
|
||||
return true if sender.nil?
|
||||
return true if sender.name == 'Customer'
|
||||
|
||||
# only apply on tweets
|
||||
return if !record.type_id
|
||||
return true if !record.type_id
|
||||
|
||||
type = Ticket::Article::Type.lookup(id: record.type_id)
|
||||
return if type['name'] !~ /\Atwitter/i
|
||||
return true if type.nil?
|
||||
return true if type.name !~ /\Atwitter/i
|
||||
|
||||
raise Exceptions::UnprocessableEntity, 'twitter to: parameter is missing' if record.to.blank? && type['name'] == 'twitter direct-message'
|
||||
|
||||
|
|
|
@ -7,10 +7,8 @@ class Observer::Ticket::Article::CommunicateTwitter::BackgroundJob
|
|||
article = Ticket::Article.find(@article_id)
|
||||
|
||||
# set retry count
|
||||
if !article.preferences['delivery_retry']
|
||||
article.preferences['delivery_retry'] = 0
|
||||
end
|
||||
article.preferences['delivery_retry'] += 1
|
||||
record.preferences['delivery_retry'] ||= 0
|
||||
record.preferences['delivery_retry'] += 1
|
||||
|
||||
ticket = Ticket.lookup(id: article.ticket_id)
|
||||
log_error(article, "Can't find ticket.preferences for Ticket.find(#{article.ticket_id})") if !ticket.preferences
|
||||
|
|
|
@ -17,13 +17,14 @@ class Observer::Ticket::Article::FillupFromEmail < ActiveRecord::Observer
|
|||
|
||||
sender = Ticket::Article::Sender.lookup(id: record.sender_id)
|
||||
return true if sender.nil?
|
||||
return true if sender['name'] == 'Customer'
|
||||
return true if sender.name == 'Customer'
|
||||
|
||||
# set email attributes
|
||||
return true if !record.type_id
|
||||
|
||||
type = Ticket::Article::Type.lookup(id: record.type_id)
|
||||
return true if type['name'] != 'email'
|
||||
return true if type.nil?
|
||||
return true if type.name != 'email'
|
||||
|
||||
# set subject if empty
|
||||
ticket = record.ticket
|
||||
|
|
|
@ -10,19 +10,21 @@ class Observer::Ticket::Article::FillupFromGeneral < ActiveRecord::Observer
|
|||
|
||||
# only do fill of from if article got created via application_server (e. g. not
|
||||
# if article and sender type is set via *.postmaster)
|
||||
return if ApplicationHandleInfo.postmaster?
|
||||
return true if ApplicationHandleInfo.postmaster?
|
||||
|
||||
# set from on all article types excluding email|twitter status|twitter direct-message|facebook feed post|facebook feed comment
|
||||
return true if record.type_id.blank?
|
||||
|
||||
type = Ticket::Article::Type.lookup(id: record.type_id)
|
||||
return true if type['name'] == 'email'
|
||||
|
||||
# from will be set by channel backend
|
||||
return true if type['name'] == 'twitter status'
|
||||
return true if type['name'] == 'twitter direct-message'
|
||||
return true if type['name'] == 'facebook feed post'
|
||||
return true if type['name'] == 'facebook feed comment'
|
||||
return true if type.nil?
|
||||
return true if type.name == 'email'
|
||||
return true if type.name == 'twitter status'
|
||||
return true if type.name == 'twitter direct-message'
|
||||
return true if type.name == 'facebook feed post'
|
||||
return true if type.name == 'facebook feed comment'
|
||||
return true if type.name == 'sms'
|
||||
|
||||
user_id = record.created_by_id
|
||||
|
||||
|
|
|
@ -6,19 +6,21 @@ class Observer::Ticket::Article::FillupFromOriginById < ActiveRecord::Observer
|
|||
def before_create(record)
|
||||
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
return true if Setting.get('import_mode')
|
||||
|
||||
# only do fill origin_by_id if article got created via application_server (e. g. not
|
||||
# if article and sender type is set via *.postmaster)
|
||||
return if ApplicationHandleInfo.postmaster?
|
||||
return true if ApplicationHandleInfo.postmaster?
|
||||
|
||||
# check if origin_by_id exists
|
||||
return if record.origin_by_id.present?
|
||||
return if record.ticket.customer_id.blank?
|
||||
return if record.sender.name != 'Customer'
|
||||
return true if record.origin_by_id.present?
|
||||
return true if record.ticket.blank?
|
||||
return true if record.ticket.customer_id.blank?
|
||||
return true if record.sender_id.blank?
|
||||
return true if record.sender.name != 'Customer'
|
||||
|
||||
type_name = record.type.name
|
||||
return if type_name != 'phone' && type_name != 'note' && type_name != 'web'
|
||||
return true if type_name != 'phone' && type_name != 'note' && type_name != 'web'
|
||||
|
||||
record.origin_by_id = record.ticket.customer_id
|
||||
end
|
||||
|
|
|
@ -918,229 +918,17 @@ perform changes on ticket
|
|||
end
|
||||
|
||||
perform_notification.each do |key, value|
|
||||
perform_changes_notification(key, value, perform_origin, article)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def perform_changes_notification(_key, value, perform_origin, article)
|
||||
|
||||
# value['recipient'] was a string in the past (single-select) so we convert it to array if needed
|
||||
value_recipient = value['recipient']
|
||||
if !value_recipient.is_a?(Array)
|
||||
value_recipient = [value_recipient]
|
||||
end
|
||||
|
||||
recipients_raw = []
|
||||
value_recipient.each do |recipient|
|
||||
if recipient == 'article_last_sender'
|
||||
if article.present?
|
||||
if article.reply_to.present?
|
||||
recipients_raw.push(article.reply_to)
|
||||
elsif article.from.present?
|
||||
recipients_raw.push(article.from)
|
||||
elsif article.origin_by_id
|
||||
email = User.find_by(id: article.origin_by_id).email
|
||||
recipients_raw.push(email)
|
||||
elsif article.created_by_id
|
||||
email = User.find_by(id: article.created_by_id).email
|
||||
recipients_raw.push(email)
|
||||
end
|
||||
end
|
||||
elsif recipient == 'ticket_customer'
|
||||
email = User.find_by(id: customer_id).email
|
||||
recipients_raw.push(email)
|
||||
elsif recipient == 'ticket_owner'
|
||||
email = User.find_by(id: owner_id).email
|
||||
recipients_raw.push(email)
|
||||
elsif recipient == 'ticket_agents'
|
||||
User.group_access(group_id, 'full').sort_by(&:login).each do |user|
|
||||
recipients_raw.push(user.email)
|
||||
end
|
||||
else
|
||||
logger.error "Unknown email notification recipient '#{recipient}'"
|
||||
# send notification
|
||||
case key
|
||||
when 'notification.sms'
|
||||
send_sms_notification(value, article, perform_origin)
|
||||
next
|
||||
when 'notification.email'
|
||||
send_email_notification(value, article, perform_origin)
|
||||
end
|
||||
end
|
||||
|
||||
recipients_checked = []
|
||||
recipients_raw.each do |recipient_email|
|
||||
|
||||
skip_user = false
|
||||
users = User.where(email: recipient_email)
|
||||
users.each do |user|
|
||||
next if user.preferences[:mail_delivery_failed] != true
|
||||
next if !user.preferences[:mail_delivery_failed_data]
|
||||
|
||||
till_blocked = ((user.preferences[:mail_delivery_failed_data] - Time.zone.now - 60.days) / 60 / 60 / 24).round
|
||||
next if till_blocked.positive?
|
||||
|
||||
logger.info "Send no trigger based notification to #{recipient_email} because email is marked as mail_delivery_failed for #{till_blocked} days"
|
||||
skip_user = true
|
||||
break
|
||||
end
|
||||
next if skip_user
|
||||
|
||||
# send notifications only to email adresses
|
||||
next if recipient_email.blank?
|
||||
next if recipient_email !~ /@/
|
||||
|
||||
# check if address is valid
|
||||
begin
|
||||
Mail::AddressList.new(recipient_email).addresses.each do |address|
|
||||
recipient_email = address.address
|
||||
break if recipient_email.present? && recipient_email =~ /@/ && !recipient_email.match?(/\s/)
|
||||
end
|
||||
rescue
|
||||
if recipient_email.present?
|
||||
if recipient_email !~ /^(.+?)<(.+?)@(.+?)>$/
|
||||
next # no usable format found
|
||||
end
|
||||
|
||||
recipient_email = "#{$2}@#{$3}"
|
||||
end
|
||||
next if recipient_email.blank?
|
||||
next if recipient_email !~ /@/
|
||||
next if recipient_email.match?(/\s/)
|
||||
end
|
||||
|
||||
# do not sent notifications to this recipients
|
||||
send_no_auto_response_reg_exp = Setting.get('send_no_auto_response_reg_exp')
|
||||
begin
|
||||
next if recipient_email.match?(/#{send_no_auto_response_reg_exp}/i)
|
||||
rescue => e
|
||||
logger.error "ERROR: Invalid regex '#{send_no_auto_response_reg_exp}' in setting send_no_auto_response_reg_exp"
|
||||
logger.error 'ERROR: ' + e.inspect
|
||||
next if recipient_email.match?(/(mailer-daemon|postmaster|abuse|root|noreply|noreply.+?|no-reply|no-reply.+?)@.+?/i)
|
||||
end
|
||||
|
||||
# check if notification should be send because of customer emails
|
||||
if article.present? && article.preferences.fetch('is-auto-response', false) == true && article.from && article.from =~ /#{Regexp.quote(recipient_email)}/i
|
||||
logger.info "Send no trigger based notification to #{recipient_email} because of auto response tagged incoming email"
|
||||
next
|
||||
end
|
||||
|
||||
# loop protection / check if maximal count of trigger mail has reached
|
||||
map = {
|
||||
10 => 10,
|
||||
30 => 15,
|
||||
60 => 25,
|
||||
180 => 50,
|
||||
600 => 100,
|
||||
}
|
||||
skip = false
|
||||
map.each do |minutes, count|
|
||||
already_sent = Ticket::Article.where(
|
||||
ticket_id: id,
|
||||
sender: Ticket::Article::Sender.find_by(name: 'System'),
|
||||
type: Ticket::Article::Type.find_by(name: 'email'),
|
||||
).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
|
||||
next if already_sent < count
|
||||
|
||||
logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} for this ticket within last #{minutes} minutes (loop protection)"
|
||||
skip = true
|
||||
break
|
||||
end
|
||||
next if skip
|
||||
|
||||
map = {
|
||||
10 => 30,
|
||||
30 => 60,
|
||||
60 => 120,
|
||||
180 => 240,
|
||||
600 => 360,
|
||||
}
|
||||
skip = false
|
||||
map.each do |minutes, count|
|
||||
already_sent = Ticket::Article.where(
|
||||
sender: Ticket::Article::Sender.find_by(name: 'System'),
|
||||
type: Ticket::Article::Type.find_by(name: 'email'),
|
||||
).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
|
||||
next if already_sent < count
|
||||
|
||||
logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{minutes} minutes (loop protection)"
|
||||
skip = true
|
||||
break
|
||||
end
|
||||
next if skip
|
||||
|
||||
email = recipient_email.downcase.strip
|
||||
next if recipients_checked.include?(email)
|
||||
|
||||
recipients_checked.push(email)
|
||||
end
|
||||
|
||||
return if recipients_checked.blank?
|
||||
|
||||
recipient_string = recipients_checked.join(', ')
|
||||
|
||||
group_id = self.group_id
|
||||
return if !group_id
|
||||
|
||||
email_address = Group.find(group_id).email_address
|
||||
if !email_address
|
||||
logger.info "Unable to send trigger based notification to #{recipient_string} because no email address is set for group '#{group.name}'"
|
||||
return
|
||||
end
|
||||
|
||||
if !email_address.channel_id
|
||||
logger.info "Unable to send trigger based notification to #{recipient_string} because no channel is set for email address '#{email_address.email}' (id: #{email_address.id})"
|
||||
return
|
||||
end
|
||||
|
||||
# articles.last breaks (returns the wrong article)
|
||||
# if another email notification trigger preceded this one
|
||||
# (see https://github.com/zammad/zammad/issues/1543)
|
||||
objects = {
|
||||
ticket: self,
|
||||
article: article || articles.last
|
||||
}
|
||||
|
||||
# get subject
|
||||
subject = NotificationFactory::Mailer.template(
|
||||
templateInline: value['subject'],
|
||||
locale: 'en-en',
|
||||
objects: objects,
|
||||
quote: false,
|
||||
)
|
||||
subject = subject_build(subject)
|
||||
|
||||
body = NotificationFactory::Mailer.template(
|
||||
templateInline: value['body'],
|
||||
locale: 'en-en',
|
||||
objects: objects,
|
||||
quote: true,
|
||||
)
|
||||
|
||||
(body, attachments_inline) = HtmlSanitizer.replace_inline_images(body, id)
|
||||
|
||||
message = Ticket::Article.create(
|
||||
ticket_id: id,
|
||||
to: recipient_string,
|
||||
subject: subject,
|
||||
content_type: 'text/html',
|
||||
body: body,
|
||||
internal: false,
|
||||
sender: Ticket::Article::Sender.find_by(name: 'System'),
|
||||
type: Ticket::Article::Type.find_by(name: 'email'),
|
||||
preferences: {
|
||||
perform_origin: perform_origin,
|
||||
},
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
attachments_inline.each do |attachment|
|
||||
Store.add(
|
||||
object: 'Ticket::Article',
|
||||
o_id: message.id,
|
||||
data: attachment[:data],
|
||||
filename: attachment[:filename],
|
||||
preferences: attachment[:preferences],
|
||||
)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
|
@ -1485,4 +1273,296 @@ result
|
|||
self.owner_id = 1
|
||||
true
|
||||
end
|
||||
|
||||
# articles.last breaks (returns the wrong article)
|
||||
# if another email notification trigger preceded this one
|
||||
# (see https://github.com/zammad/zammad/issues/1543)
|
||||
def build_notification_template_objects(article)
|
||||
{
|
||||
ticket: self,
|
||||
article: article || articles.last
|
||||
}
|
||||
end
|
||||
|
||||
def send_email_notification(value, article, perform_origin)
|
||||
# value['recipient'] was a string in the past (single-select) so we convert it to array if needed
|
||||
value_recipient = Array(value['recipient'])
|
||||
|
||||
recipients_raw = []
|
||||
value_recipient.each do |recipient|
|
||||
if recipient == 'article_last_sender'
|
||||
if article.present?
|
||||
if article.reply_to.present?
|
||||
recipients_raw.push(article.reply_to)
|
||||
elsif article.from.present?
|
||||
recipients_raw.push(article.from)
|
||||
elsif article.origin_by_id
|
||||
email = User.find_by(id: article.origin_by_id).email
|
||||
recipients_raw.push(email)
|
||||
elsif article.created_by_id
|
||||
email = User.find_by(id: article.created_by_id).email
|
||||
recipients_raw.push(email)
|
||||
end
|
||||
end
|
||||
elsif recipient == 'ticket_customer'
|
||||
email = User.find_by(id: customer_id).email
|
||||
recipients_raw.push(email)
|
||||
elsif recipient == 'ticket_owner'
|
||||
email = User.find_by(id: owner_id).email
|
||||
recipients_raw.push(email)
|
||||
elsif recipient == 'ticket_agents'
|
||||
User.group_access(group_id, 'full').sort_by(&:login).each do |user|
|
||||
recipients_raw.push(user.email)
|
||||
end
|
||||
else
|
||||
logger.error "Unknown email notification recipient '#{recipient}'"
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
recipients_checked = []
|
||||
recipients_raw.each do |recipient_email|
|
||||
|
||||
skip_user = false
|
||||
users = User.where(email: recipient_email)
|
||||
users.each do |user|
|
||||
next if user.preferences[:mail_delivery_failed] != true
|
||||
next if !user.preferences[:mail_delivery_failed_data]
|
||||
|
||||
till_blocked = ((user.preferences[:mail_delivery_failed_data] - Time.zone.now - 60.days) / 60 / 60 / 24).round
|
||||
next if till_blocked.positive?
|
||||
|
||||
logger.info "Send no trigger based notification to #{recipient_email} because email is marked as mail_delivery_failed for #{till_blocked} days"
|
||||
skip_user = true
|
||||
break
|
||||
end
|
||||
next if skip_user
|
||||
|
||||
# send notifications only to email adresses
|
||||
next if recipient_email.blank?
|
||||
next if recipient_email !~ /@/
|
||||
|
||||
# check if address is valid
|
||||
begin
|
||||
Mail::AddressList.new(recipient_email).addresses.each do |address|
|
||||
recipient_email = address.address
|
||||
break if recipient_email.present? && recipient_email =~ /@/ && !recipient_email.match?(/\s/)
|
||||
end
|
||||
rescue
|
||||
if recipient_email.present?
|
||||
if recipient_email !~ /^(.+?)<(.+?)@(.+?)>$/
|
||||
next # no usable format found
|
||||
end
|
||||
|
||||
recipient_email = "#{$2}@#{$3}"
|
||||
end
|
||||
next if recipient_email.blank?
|
||||
next if recipient_email !~ /@/
|
||||
next if recipient_email.match?(/\s/)
|
||||
end
|
||||
|
||||
# do not sent notifications to this recipients
|
||||
send_no_auto_response_reg_exp = Setting.get('send_no_auto_response_reg_exp')
|
||||
begin
|
||||
next if recipient_email.match?(/#{send_no_auto_response_reg_exp}/i)
|
||||
rescue => e
|
||||
logger.error "ERROR: Invalid regex '#{send_no_auto_response_reg_exp}' in setting send_no_auto_response_reg_exp"
|
||||
logger.error 'ERROR: ' + e.inspect
|
||||
next if recipient_email.match?(/(mailer-daemon|postmaster|abuse|root|noreply|noreply.+?|no-reply|no-reply.+?)@.+?/i)
|
||||
end
|
||||
|
||||
# check if notification should be send because of customer emails
|
||||
if article.present? && article.preferences.fetch('is-auto-response', false) == true && article.from && article.from =~ /#{Regexp.quote(recipient_email)}/i
|
||||
logger.info "Send no trigger based notification to #{recipient_email} because of auto response tagged incoming email"
|
||||
next
|
||||
end
|
||||
|
||||
# loop protection / check if maximal count of trigger mail has reached
|
||||
map = {
|
||||
10 => 10,
|
||||
30 => 15,
|
||||
60 => 25,
|
||||
180 => 50,
|
||||
600 => 100,
|
||||
}
|
||||
skip = false
|
||||
map.each do |minutes, count|
|
||||
already_sent = Ticket::Article.where(
|
||||
ticket_id: id,
|
||||
sender: Ticket::Article::Sender.find_by(name: 'System'),
|
||||
type: Ticket::Article::Type.find_by(name: 'email'),
|
||||
).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
|
||||
next if already_sent < count
|
||||
|
||||
logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} for this ticket within last #{minutes} minutes (loop protection)"
|
||||
skip = true
|
||||
break
|
||||
end
|
||||
next if skip
|
||||
|
||||
map = {
|
||||
10 => 30,
|
||||
30 => 60,
|
||||
60 => 120,
|
||||
180 => 240,
|
||||
600 => 360,
|
||||
}
|
||||
skip = false
|
||||
map.each do |minutes, count|
|
||||
already_sent = Ticket::Article.where(
|
||||
sender: Ticket::Article::Sender.find_by(name: 'System'),
|
||||
type: Ticket::Article::Type.find_by(name: 'email'),
|
||||
).where('ticket_articles.created_at > ? AND ticket_articles.to LIKE ?', Time.zone.now - minutes.minutes, "%#{recipient_email.strip}%").count
|
||||
next if already_sent < count
|
||||
|
||||
logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{minutes} minutes (loop protection)"
|
||||
skip = true
|
||||
break
|
||||
end
|
||||
next if skip
|
||||
|
||||
email = recipient_email.downcase.strip
|
||||
next if recipients_checked.include?(email)
|
||||
|
||||
recipients_checked.push(email)
|
||||
end
|
||||
|
||||
return if recipients_checked.blank?
|
||||
|
||||
recipient_string = recipients_checked.join(', ')
|
||||
|
||||
group_id = self.group_id
|
||||
return if !group_id
|
||||
|
||||
email_address = Group.find(group_id).email_address
|
||||
if !email_address
|
||||
logger.info "Unable to send trigger based notification to #{recipient_string} because no email address is set for group '#{group.name}'"
|
||||
return
|
||||
end
|
||||
|
||||
if !email_address.channel_id
|
||||
logger.info "Unable to send trigger based notification to #{recipient_string} because no channel is set for email address '#{email_address.email}' (id: #{email_address.id})"
|
||||
return
|
||||
end
|
||||
|
||||
objects = build_notification_template_objects(article)
|
||||
|
||||
# get subject
|
||||
subject = NotificationFactory::Mailer.template(
|
||||
templateInline: value['subject'],
|
||||
locale: 'en-en',
|
||||
objects: objects,
|
||||
quote: false,
|
||||
)
|
||||
subject = subject_build(subject)
|
||||
|
||||
body = NotificationFactory::Mailer.template(
|
||||
templateInline: value['body'],
|
||||
locale: 'en-en',
|
||||
objects: objects,
|
||||
quote: true,
|
||||
)
|
||||
|
||||
(body, attachments_inline) = HtmlSanitizer.replace_inline_images(body, id)
|
||||
|
||||
message = Ticket::Article.create(
|
||||
ticket_id: id,
|
||||
to: recipient_string,
|
||||
subject: subject,
|
||||
content_type: 'text/html',
|
||||
body: body,
|
||||
internal: false,
|
||||
sender: Ticket::Article::Sender.find_by(name: 'System'),
|
||||
type: Ticket::Article::Type.find_by(name: 'email'),
|
||||
preferences: {
|
||||
perform_origin: perform_origin,
|
||||
},
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
attachments_inline.each do |attachment|
|
||||
Store.add(
|
||||
object: 'Ticket::Article',
|
||||
o_id: message.id,
|
||||
data: attachment[:data],
|
||||
filename: attachment[:filename],
|
||||
preferences: attachment[:preferences],
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def sms_recipients_by_type(recipient_type, article)
|
||||
case recipient_type
|
||||
when 'article_last_sender'
|
||||
return nil if article.blank?
|
||||
|
||||
if article.origin_by_id
|
||||
article.origin_by_id
|
||||
elsif article.created_by_id
|
||||
article.created_by_id
|
||||
end
|
||||
when 'ticket_customer'
|
||||
customer_id
|
||||
when 'ticket_owner'
|
||||
owner_id
|
||||
when 'ticket_agents'
|
||||
User.group_access(group_id, 'full').sort_by(&:login)
|
||||
else
|
||||
logger.error "Unknown sms notification recipient '#{recipient}'"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def build_sms_recipients_list(value, article)
|
||||
Array(value['recipient'])
|
||||
.each_with_object([]) { |recipient_type, sum| sum.concat(Array(sms_recipients_by_type(recipient_type, article))) }
|
||||
.map { |user_or_id| User.lookup(id: user_or_id) }
|
||||
.select { |user| user.mobile.present? }
|
||||
end
|
||||
|
||||
def send_sms_notification(value, article, perform_origin)
|
||||
sms_recipients = build_sms_recipients_list(value, article)
|
||||
|
||||
if sms_recipients.blank?
|
||||
logger.debug "No SMS recipients found for Ticket# #{number}"
|
||||
return
|
||||
end
|
||||
|
||||
sms_recipients_to = sms_recipients
|
||||
.map { |recipient| "#{recipient.fullname} (#{recipient.mobile})" }
|
||||
.join(', ')
|
||||
|
||||
channel = Channel.find_by(area: 'Sms::Notification')
|
||||
if !channel.active?
|
||||
# write info message since we have an active trigger
|
||||
logger.info "Found possible SMS recipient(s) (#{sms_recipients_to}) for Ticket# #{number} but SMS channel is not active."
|
||||
return
|
||||
end
|
||||
|
||||
objects = build_notification_template_objects(article)
|
||||
body = NotificationFactory::Renderer.new(objects, 'en-en', value['body'], false)
|
||||
.render
|
||||
.html2text
|
||||
.tr(' ', ' ') # convert non-breaking space to simple space
|
||||
|
||||
# attributes content_type is not needed for SMS
|
||||
article = Ticket::Article.create(
|
||||
ticket_id: id,
|
||||
subject: 'SMS notification',
|
||||
to: sms_recipients_to,
|
||||
body: body,
|
||||
internal: true,
|
||||
sender: Ticket::Article::Sender.find_by(name: 'System'),
|
||||
type: Ticket::Article::Type.find_by(name: 'sms'),
|
||||
preferences: {
|
||||
perform_origin: perform_origin,
|
||||
sms_recipients: sms_recipients.map(&:mobile),
|
||||
channel_id: channel.id,
|
||||
},
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,6 +23,10 @@ class Ticket::Article < ApplicationModel
|
|||
|
||||
store :preferences
|
||||
|
||||
validates :ticket_id, presence: true
|
||||
validates :type_id, presence: true
|
||||
validates :sender_id, presence: true
|
||||
|
||||
sanitized_html :body
|
||||
|
||||
activity_stream_permission 'ticket.agent'
|
||||
|
|
|
@ -23,7 +23,6 @@ class Transaction::Trigger
|
|||
end
|
||||
|
||||
def perform
|
||||
|
||||
# return if we run import mode
|
||||
return if Setting.get('import_mode')
|
||||
|
||||
|
@ -39,7 +38,6 @@ class Transaction::Trigger
|
|||
original_user_id = UserInfo.current_user_id
|
||||
|
||||
Ticket.perform_triggers(ticket, article, @item, @params)
|
||||
|
||||
UserInfo.current_user_id = original_user_id
|
||||
end
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ module Zammad
|
|||
'observer::_ticket::_article::_fillup_from_email',
|
||||
'observer::_ticket::_article::_communicate_email',
|
||||
'observer::_ticket::_article::_communicate_facebook',
|
||||
'observer::_ticket::_article::_communicate_sms',
|
||||
'observer::_ticket::_article::_communicate_twitter',
|
||||
'observer::_ticket::_article::_communicate_telegram',
|
||||
'observer::_ticket::_reset_new_state',
|
||||
|
|
16
config/routes/channel_sms.rb
Normal file
16
config/routes/channel_sms.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
Zammad::Application.routes.draw do
|
||||
api_path = Rails.configuration.api_path
|
||||
|
||||
match api_path + '/channels_sms', to: 'channels_sms#index', via: :get
|
||||
match api_path + '/channels_sms/:id', to: 'channels_sms#show', via: :get
|
||||
match api_path + '/channels_sms', to: 'channels_sms#create', via: :post
|
||||
match api_path + '/channels_sms/:id', to: 'channels_sms#update', via: :put
|
||||
match api_path + '/channels_sms/:id', to: 'channels_sms#destroy', via: :delete
|
||||
match api_path + '/channels_sms_enable', to: 'channels_sms#enable', via: :post
|
||||
match api_path + '/channels_sms_disable', to: 'channels_sms#disable', via: :post
|
||||
match api_path + '/channels_sms', to: 'channels_sms#destroy', via: :delete
|
||||
match api_path + '/channels_sms/test', to: 'channels_sms#test', via: :post
|
||||
match api_path + '/sms_webhook/:token', to: 'channels_sms#webhook', via: :get
|
||||
match api_path + '/sms_webhook/:token', to: 'channels_sms#webhook', via: :post
|
||||
|
||||
end
|
Binary file not shown.
15
db/migrate/20180524182518_sms_support.rb
Normal file
15
db/migrate/20180524182518_sms_support.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
class SmsSupport < ActiveRecord::Migration[5.1]
|
||||
def up
|
||||
|
||||
# return if it's a new setup
|
||||
return if !Setting.find_by(name: 'system_init_done')
|
||||
|
||||
Permission.create_if_not_exists(
|
||||
name: 'admin.channel_sms',
|
||||
note: 'Manage %s',
|
||||
preferences: {
|
||||
translations: ['Channel - SMS']
|
||||
},
|
||||
)
|
||||
end
|
||||
end
|
|
@ -136,6 +136,13 @@ Permission.create_if_not_exists(
|
|||
translations: ['Channel - Telegram']
|
||||
},
|
||||
)
|
||||
Permission.create_if_not_exists(
|
||||
name: 'admin.channel_sms',
|
||||
note: 'Manage %s',
|
||||
preferences: {
|
||||
translations: ['Channel - SMS']
|
||||
},
|
||||
)
|
||||
Permission.create_if_not_exists(
|
||||
name: 'admin.channel_chat',
|
||||
note: 'Manage %s',
|
||||
|
|
|
@ -92,15 +92,33 @@ examples how to use
|
|||
break
|
||||
end
|
||||
|
||||
arguments = nil
|
||||
if /\A(?<method_id>[^\(]+)\((?<parameter>[^\)]+)\)\z/ =~ method
|
||||
|
||||
if parameter != parameter.to_i.to_s
|
||||
value = "\#{#{object_name}.#{object_methods_s} / invalid parameter: #{parameter}}"
|
||||
break
|
||||
end
|
||||
|
||||
begin
|
||||
arguments = Array(parameter.to_i)
|
||||
method = method_id
|
||||
rescue
|
||||
value = "\#{#{object_name}.#{object_methods_s} / #{e.message}}"
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
# if method exists
|
||||
if !object_refs.respond_to?(method.to_sym)
|
||||
value = "\#{#{object_name}.#{object_methods_s} / no such method}"
|
||||
break
|
||||
end
|
||||
begin
|
||||
object_refs = object_refs.send(method.to_sym)
|
||||
object_refs = object_refs.send(method.to_sym, *arguments)
|
||||
rescue => e
|
||||
object_refs = "\#{#{object_name}.#{object_methods_s} / e.message}"
|
||||
value = "\#{#{object_name}.#{object_methods_s} / #{e.message}}"
|
||||
break
|
||||
end
|
||||
end
|
||||
placeholder = if !value
|
||||
|
|
|
@ -405,11 +405,21 @@
|
|||
logotype
|
||||
</title>
|
||||
<path d="M0 14.752h11.657V9.697H8.563v2.167h-3.24a8.08 8.08 0 0 0-.887.062c0-.042.371-.33.66-.743l6.314-8.955V0H.083v4.972h3.094V2.89h2.889c.413 0 .887-.062.887-.062 0 .041-.371.33-.66.742L0 12.483v2.27zm13.081-3.033c0 2.146 1.63 3.281 3.487 3.281 2.27 0 3.136-1.754 3.136-1.754h.042s-.042.145-.042.33v.062c0 .516.392 1.114 1.424 1.114h3.074v-2.579h-.784c-.289 0-.454-.165-.454-.454V8.212c0-2.786-1.65-4.333-4.745-4.333-2.683 0-4.498 1.382-4.498 1.382l1.238 2.414s1.485-1.01 2.93-1.01c.804 0 1.506.309 1.506 1.175v.31h-.64c-1.65 0-5.674.454-5.674 3.57zm3.59-.247c0-.95 1.259-1.32 2.435-1.32h.33v.247c0 .928-.908 2.022-1.816 2.022-.577 0-.949-.392-.949-.95zm8.831 3.28h6.252v-2.579h-1.341v-1.609c0-1.713.412-3.446 2.042-3.446.681 0 .826.557.826 1.341v6.293h4.91v-2.579h-1.34v-1.815c0-1.775.494-3.24 2.042-3.24.68 0 .825.557.825 1.341v6.293h4.91v-2.579h-1.34V7.861c0-2.93-1.527-3.982-3.343-3.982-1.63 0-2.93.867-3.508 2.001h-.04c-.517-1.485-1.61-2.001-2.766-2.001-1.795 0-2.826 1.217-3.363 2.043h-.041s.02-.145.02-.248v-.35c0-.723-.515-1.197-1.485-1.197h-3.363v2.579h.99c.29 0 .454.144.454.433v5.034h-1.34v2.58zm20.447 0h6.252v-2.579H50.86v-1.609c0-1.713.412-3.446 2.042-3.446.681 0 .826.557.826 1.341v6.293h4.91v-2.579h-1.34v-1.815c0-1.775.494-3.24 2.042-3.24.68 0 .825.557.825 1.341v6.293h4.91v-2.579h-1.34V7.861c0-2.93-1.527-3.982-3.343-3.982-1.63 0-2.93.867-3.508 2.001h-.04c-.517-1.485-1.61-2.001-2.766-2.001-1.795 0-2.826 1.217-3.363 2.043h-.041s.02-.145.02-.248v-.35c0-.723-.515-1.197-1.485-1.197h-3.363v2.579h.99c.29 0 .454.144.454.433v5.034h-1.34v2.58zm20.055-3.033c0 2.146 1.63 3.281 3.487 3.281 2.27 0 3.136-1.754 3.136-1.754h.042s-.042.145-.042.33v.062c0 .516.392 1.114 1.424 1.114h3.074v-2.579h-.784c-.289 0-.454-.165-.454-.454V8.212c0-2.786-1.65-4.333-4.745-4.333-2.683 0-4.498 1.382-4.498 1.382l1.238 2.414s1.485-1.01 2.93-1.01c.804 0 1.506.309 1.506 1.175v.31h-.64c-1.65 0-5.674.454-5.674 3.57zm3.59-.247c0-.95 1.259-1.32 2.435-1.32h.33v.247c0 .928-.908 2.022-1.816 2.022-.577 0-.949-.392-.949-.95zm8.522-2.022c0 3.28 1.856 5.55 4.683 5.55 2.476 0 3.28-1.754 3.28-1.754h.042s-.041.145-.041.33v.062c0 .516.392 1.114 1.423 1.114h3.075v-2.579h-.784c-.29 0-.454-.165-.454-.454V0h-5.014v2.58h1.444v1.67c0 .392.02.681.02.681h-.04s-.681-1.052-2.724-1.052c-2.93 0-4.91 2.187-4.91 5.57zm3.59 0c0-1.65.949-2.559 2.125-2.559 1.362 0 2.042 1.238 2.042 2.641 0 1.713-1.031 2.518-2.104 2.518-1.217 0-2.063-1.053-2.063-2.6z" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-long-arrow-down" viewBox="0 0 11 11">
|
||||
<title>
|
||||
long-arrow-down
|
||||
</title>
|
||||
<path d="M4.81 8.605c-.24-.33-.576-.66-.96-1.024L1.412 5.27.5 6.375 5.5 11l5-4.625-.913-1.106-2.308 2.196c-.465.43-.833.777-1.09 1.14l-.048-.016V0H4.859v8.589l-.048.016z" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-long-arrow-right" viewBox="0 0 11 11">
|
||||
<title>
|
||||
long-arrow-right
|
||||
</title>
|
||||
<path d="M8.347 6.203c-.32.24-.64.577-.993.962L5.11 9.6l1.073.913 4.486-4.998L6.184.516l-1.073.913 2.13 2.307c.417.465.753.833 1.106 1.09l-.016.048H0v1.281h8.33l.017.048z" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-low-priority" viewBox="0 0 16 16">
|
||||
<title>
|
||||
low-priority
|
||||
</title>
|
||||
<path d="M7 11.18L3.886 8.031a1 1 0 0 0-1.41 0l.093-.094a.994.994 0 0 0-.008 1.411l4.719 4.733a.988.988 0 0 0 1.404 0l4.714-4.733c.388-.39.393-1.024.005-1.426l.119.124a.973.973 0 0 0-1.399-.015l-3.121 3.15V1.961a.997.997 0 0 0-1-.998c-.553 0-1 .44-1 .998v9.218z" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-magnifier" viewBox="0 0 15 15">
|
||||
<title>
|
||||
magnifier
|
||||
|
@ -634,6 +644,11 @@
|
|||
small-dot
|
||||
</title>
|
||||
<circle cx="8" cy="8" r="3" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-sms" viewBox="0 0 17 17">
|
||||
<title>
|
||||
sms
|
||||
</title>
|
||||
<path d="M7.616 13.923L3.25 16.75l.714-3.847C1.582 11.69 0 9.546 0 7.105 0 3.319 3.806.25 8.5.25S17 3.319 17 7.105c0 3.786-3.806 6.855-8.5 6.855-.298 0-.593-.013-.884-.037zm-3.71-9.248c-.984 0-1.708.522-1.708 1.338 0 .689.39 1.117 1.448 1.438.69.208.868.361.868.703 0 .354-.287.568-.752.568-.47 0-.847-.167-1.195-.455L2 8.877c.39.367.977.661 1.797.661 1.181 0 1.878-.602 1.878-1.465 0-.856-.512-1.217-1.407-1.498-.738-.228-.915-.348-.915-.642 0-.295.246-.455.635-.455.382 0 .71.127 1.038.388l.52-.596c-.417-.388-.916-.595-1.64-.595zm6.729.114H9.214L8.55 7.933l-.71-3.144H6.427L6.07 9.425h1.093l.082-1.8c.028-.649.028-1.224-.02-1.906l.779 3.217h1.059l.73-3.217c-.034.575-.02 1.244.014 1.893l.082 1.813h1.1l-.355-4.636zm2.596-.114c-.984 0-1.708.522-1.708 1.338 0 .689.39 1.117 1.448 1.438.69.208.868.361.868.703 0 .354-.287.568-.752.568-.471 0-.847-.167-1.195-.455l-.567.61c.39.367.977.661 1.796.661 1.182 0 1.879-.602 1.879-1.465 0-.856-.512-1.217-1.407-1.498-.738-.228-.916-.348-.916-.642 0-.295.246-.455.636-.455.382 0 .71.127 1.038.388l.52-.596c-.418-.388-.916-.595-1.64-.595z" fill-rule="evenodd"/>
|
||||
</symbol><symbol id="icon-split" viewBox="0 0 16 17">
|
||||
<title>
|
||||
split
|
||||
|
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 76 KiB |
9
public/assets/images/icons/sms.svg
Normal file
9
public/assets/images/icons/sms.svg
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="17px" height="17px" viewBox="0 0 17 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.2 (67145) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>sms</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="sms" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M7.61649155,13.9230946 L3.25,16.75 L3.9643919,12.9032744 C1.58206933,11.6887873 0,9.54580983 0,7.10483871 C0,3.31901583 3.80557963,0.25 8.5,0.25 C13.1944204,0.25 17,3.31901583 17,7.10483871 C17,10.8906616 13.1944204,13.9596774 8.5,13.9596774 C8.20165766,13.9596774 7.90690529,13.9472819 7.61649155,13.9230946 Z M3.90593799,4.675 C2.92222806,4.675 2.19810825,5.19679314 2.19810825,6.01293113 C2.19810825,6.70196567 2.58749343,7.13010363 3.64634787,7.4512071 C4.33631109,7.65858643 4.51392538,7.81244851 4.51392538,8.15362095 C4.51392538,8.5081727 4.22700998,8.72224168 3.76248029,8.72224168 C3.29111929,8.72224168 2.91539674,8.55500029 2.56699947,8.2673451 L2,8.87610376 C2.38938518,9.24403482 2.97687861,9.53837967 3.79663689,9.53837967 C4.97845507,9.53837967 5.67524961,8.93631066 5.67524961,8.07334508 C5.67524961,7.21706916 5.16290068,6.85582775 4.2679979,6.57486221 C3.53021545,6.34741392 3.35260116,6.22700012 3.35260116,5.93265527 C3.35260116,5.63831042 3.59852864,5.47775868 3.98791382,5.47775868 C4.37046768,5.47775868 4.69837099,5.60486214 5.0262743,5.86575871 L5.54545455,5.27037935 C5.12874409,4.88237933 4.6300578,4.675 3.90593799,4.675 Z M10.6347872,4.78872415 L9.21387283,4.78872415 L8.55123489,7.93286231 L7.84077772,4.78872415 L6.42669469,4.78872415 L6.07146611,9.42465553 L7.16447714,9.42465553 L7.24645297,7.62513815 C7.27377824,6.97624155 7.27377824,6.40093116 7.22595901,5.71858629 L8.00472937,8.93631066 L9.06358382,8.93631066 L9.79453494,5.71858629 C9.76037835,6.29389667 9.77404099,6.96286224 9.80819758,7.61175884 L9.89017341,9.42465553 L10.9900158,9.42465553 L10.6347872,4.78872415 Z M13.2306884,4.675 C12.2469785,4.675 11.5228586,5.19679314 11.5228586,6.01293113 C11.5228586,6.70196567 11.9122438,7.13010363 12.9710983,7.4512071 C13.6610615,7.65858643 13.8386758,7.81244851 13.8386758,8.15362095 C13.8386758,8.5081727 13.5517604,8.72224168 13.0872307,8.72224168 C12.6158697,8.72224168 12.2401471,8.55500029 11.8917499,8.2673451 L11.3247504,8.87610376 C11.7141356,9.24403482 12.301629,9.53837967 13.1213873,9.53837967 C14.3032055,9.53837967 15,8.93631066 15,8.07334508 C15,7.21706916 14.4876511,6.85582775 13.5927483,6.57486221 C12.8549658,6.34741392 12.6773516,6.22700012 12.6773516,5.93265527 C12.6773516,5.63831042 12.923279,5.47775868 13.3126642,5.47775868 C13.6952181,5.47775868 14.0231214,5.60486214 14.3510247,5.86575871 L14.8702049,5.27037935 C14.4534945,4.88237933 13.9548082,4.675 13.2306884,4.675 Z" id="sms-bubble" fill="#50E3C2"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
46
spec/controllers/integration/exchange_spec.rb
Normal file
46
spec/controllers/integration/exchange_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Exchange integration endpoint', type: :request do
|
||||
before { authenticated_as(admin_with_admin_user_permissions) }
|
||||
|
||||
let(:admin_with_admin_user_permissions) do
|
||||
create(:user, roles: [role_with_admin_user_permissions])
|
||||
end
|
||||
|
||||
let(:role_with_admin_user_permissions) do
|
||||
create(:role).tap { |role| role.permission_grant('admin.integration') }
|
||||
end
|
||||
|
||||
describe 'EWS folder retrieval' do
|
||||
# see https://github.com/zammad/zammad/issues/1802
|
||||
context 'when no folders found (#1802)' do
|
||||
let(:empty_folder_list) { { folders: {} } }
|
||||
|
||||
it 'responds with an error message' do
|
||||
allow(Sequencer).to receive(:process).with(any_args).and_return(empty_folder_list)
|
||||
|
||||
post api_v1_integration_exchange_folders_path,
|
||||
params: {}, as: :json
|
||||
|
||||
expect(json_response).to include('result' => 'failed').and include('message')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'autodiscovery' do
|
||||
# see https://github.com/zammad/zammad/issues/2065
|
||||
context 'when Autodiscover gem raises Errno::EADDRNOTAVAIL (#2065)' do
|
||||
let(:client) { instance_double('Autodiscover::Client') }
|
||||
|
||||
it 'rescues and responds with an empty hash (to proceed to manual configuration)' do
|
||||
allow(Autodiscover::Client).to receive(:new).with(any_args).and_return(client)
|
||||
allow(client).to receive(:autodiscover).and_raise(Errno::EADDRNOTAVAIL)
|
||||
|
||||
post api_v1_integration_exchange_autodiscover_path,
|
||||
params: {}, as: :json
|
||||
|
||||
expect(json_response).to eq('result' => 'ok')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,9 +1,11 @@
|
|||
FactoryBot.define do
|
||||
factory :channel do
|
||||
area 'Email::Dummy'
|
||||
group { ::Group.find(1) }
|
||||
active true
|
||||
options {}
|
||||
preferences {}
|
||||
area 'Email::Dummy'
|
||||
group { ::Group.find(1) }
|
||||
active true
|
||||
options {}
|
||||
preferences {}
|
||||
updated_by_id 1
|
||||
created_by_id 1
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,7 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe Import::Exchange::Folder do
|
||||
# see https://github.com/zammad/zammad/issues/2152
|
||||
|
||||
describe '#display_path (#2152)' do
|
||||
let(:subject) { described_class.new(ews_connection) }
|
||||
let(:ews_connection) { Viewpoint::EWSClient.new(endpoint, user, pass) }
|
||||
|
|
72
spec/models/channel/driver/sms/massenversand_spec.rb
Normal file
72
spec/models/channel/driver/sms/massenversand_spec.rb
Normal file
|
@ -0,0 +1,72 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Channel::Driver::Sms::Massenversand do
|
||||
it 'passes' do
|
||||
channel = create_channel
|
||||
|
||||
stub_request(:get, url_to_mock)
|
||||
.to_return(body: 'OK')
|
||||
|
||||
api = channel.driver_instance.new
|
||||
expect(api.send(channel.options, { recipient: receiver_number, message: message_body })).to be true
|
||||
end
|
||||
|
||||
it 'fails' do
|
||||
channel = create_channel
|
||||
|
||||
stub_request(:get, url_to_mock)
|
||||
.to_return(body: 'blocked receiver ()')
|
||||
|
||||
api = channel.driver_instance.new
|
||||
expect { api.send(channel.options, { recipient: receiver_number, message: message_body }) }.to raise_exception(RuntimeError)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_channel
|
||||
FactoryBot.create(:channel,
|
||||
options: {
|
||||
adapter: 'sms/massenversand',
|
||||
gateway: gateway,
|
||||
sender: sender_number,
|
||||
token: token
|
||||
},
|
||||
created_by_id: 1,
|
||||
updated_by_id: 1)
|
||||
end
|
||||
|
||||
def url_to_mock
|
||||
params = {
|
||||
authToken: token,
|
||||
getID: 1,
|
||||
msg: message_body,
|
||||
msgtype: 'c',
|
||||
receiver: receiver_number,
|
||||
sender: sender_number
|
||||
}
|
||||
|
||||
gateway + '?' + URI.encode_www_form(params)
|
||||
end
|
||||
|
||||
# api parameters
|
||||
|
||||
def gateway
|
||||
'https://gate1.goyyamobile.com/sms/sendsms.asp'
|
||||
end
|
||||
|
||||
def message_body
|
||||
'Test'
|
||||
end
|
||||
|
||||
def receiver_number
|
||||
'+37060010000'
|
||||
end
|
||||
|
||||
def sender_number
|
||||
'+491000000000'
|
||||
end
|
||||
|
||||
def token
|
||||
'00q1234123423r5rwefdfsfsfef'
|
||||
end
|
||||
end
|
71
spec/models/channel/driver/sms/twilio_spec.rb
Normal file
71
spec/models/channel/driver/sms/twilio_spec.rb
Normal file
|
@ -0,0 +1,71 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Channel::Driver::Sms::Twilio do
|
||||
it 'passes' do
|
||||
channel = create_channel
|
||||
|
||||
stub_request(:post, url_to_mock)
|
||||
.to_return(body: mocked_response_success)
|
||||
|
||||
api = channel.driver_instance.new
|
||||
expect(api.send(channel.options, { recipient: '+37060010000', message: message_body })).to be true
|
||||
end
|
||||
|
||||
it 'fails' do
|
||||
channel = create_channel
|
||||
|
||||
stub_request(:post, url_to_mock)
|
||||
.to_return(status: 400, body: mocked_response_failure)
|
||||
|
||||
api = channel.driver_instance.new
|
||||
|
||||
expect { api.send(channel.options, { recipient: 'asd', message: message_body }) }.to raise_exception(Twilio::REST::RestError)
|
||||
expect(a_request(:post, url_to_mock)).to have_been_made
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_channel
|
||||
FactoryBot.create(:channel,
|
||||
options: {
|
||||
account_id: account_id,
|
||||
adapter: 'sms/twilio',
|
||||
sender: sender_number,
|
||||
token: token
|
||||
},
|
||||
created_by_id: 1,
|
||||
updated_by_id: 1)
|
||||
end
|
||||
|
||||
# api parameters
|
||||
|
||||
def url_to_mock
|
||||
"https://api.twilio.com/2010-04-01/Accounts/#{account_id}/Messages.json"
|
||||
end
|
||||
|
||||
def account_id
|
||||
'ASDASDAS3213424AD'
|
||||
end
|
||||
|
||||
def message_body
|
||||
'Test'
|
||||
end
|
||||
|
||||
def sender_number
|
||||
'+15005550006'
|
||||
end
|
||||
|
||||
def token
|
||||
'2345r4erfdvc4wedxv3efds'
|
||||
end
|
||||
|
||||
# mocked responses
|
||||
|
||||
def mocked_response_success
|
||||
'{"sid": "SM07eab0404df148a4bf3712cb8b72e4c2", "date_created": "Fri, 01 Jun 2018 06:11:19 +0000", "date_updated": "Fri, 01 Jun 2018 06:11:19 +0000", "date_sent": null, "account_sid": "AC5989ff24c08f701b8b1ef09e1b79cbf8", "to": "+37060010000", "from": "+15005550006", "messaging_service_sid": null, "body": "Sent from your Twilio trial account - Test", "status": "queued", "num_segments": "1", "num_media": "0", "direction": "outbound-api", "api_version": "2010-04-01", "price": null, "price_unit": "USD", "error_code": null, "error_message": null, "uri": "/2010-04-01/Accounts/AC5989ff24c08f701b8b1ef09e1b79cbf8/Messages/SM07eab0404df148a4bf3712cb8b72e4c2.json", "subresource_uris": {"media": "/2010-04-01/Accounts/AC5989ff24c08f701b8b1ef09e1b79cbf8/Messages/SM07eab0404df148a4bf3712cb8b72e4c2/Media.json"}}'
|
||||
end
|
||||
|
||||
def mocked_response_failure
|
||||
'{"code": 21211, "message": "The \'To\' number asd is not a valid phone number.", "more_info": "https://www.twilio.com/docs/errors/21211", "status": 400}'
|
||||
end
|
||||
end
|
32
spec/models/trigger/sms_spec.rb
Normal file
32
spec/models/trigger/sms_spec.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Trigger do
|
||||
|
||||
describe 'sms' do
|
||||
|
||||
it 'sends interpolated, html-free SMS' do
|
||||
customer = create(:customer_user)
|
||||
agent = create(:agent_user)
|
||||
another_agent = create(:admin_user, mobile: '+37061010000')
|
||||
Group.lookup(id: 1).users << another_agent
|
||||
|
||||
channel = create(:channel, area: 'Sms::Notification')
|
||||
trigger = create(:trigger,
|
||||
disable_notification: false,
|
||||
perform: {
|
||||
'notification.sms': {
|
||||
recipient: 'ticket_agents',
|
||||
body: 'space between #{ticket.title}', # rubocop:disable Lint/InterpolationCheck
|
||||
}
|
||||
})
|
||||
|
||||
ticket = create(:ticket, customer: customer, created_by_id: agent.id)
|
||||
Observer::Transaction.commit
|
||||
|
||||
triggered_article = Ticket::Article.last
|
||||
|
||||
expect(triggered_article.body.match?(/space between/)).to be_truthy
|
||||
expect(triggered_article.body.match?(ticket.title)).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
21
test/data/twilio/inbound_sms1.json
Normal file
21
test/data/twilio/inbound_sms1.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"ToCountry": "DE",
|
||||
"ToState": "",
|
||||
"SmsMessageSid": "SM1bd5330f81336e8b54c2d3d9fc1bcb6a",
|
||||
"NumMedia": "0",
|
||||
"ToCity": "",
|
||||
"FromZip": "",
|
||||
"SmsSid": "SM1bd5330f81336e8b54c2d3d9fc1bcb6a",
|
||||
"FromState": "",
|
||||
"SmsStatus": "received",
|
||||
"FromCity": "",
|
||||
"Body": "Ldfhxhcuffufuf. Fifififig. Fifififiif Fifififiif Fifififiif Fifififiif Fifififiif",
|
||||
"FromCountry": "DE",
|
||||
"To": "+4915700000000",
|
||||
"ToZip": "",
|
||||
"NumSegments": "1",
|
||||
"MessageSid": "SM1bd5330f81336e8b54c2d3d9fc1bcb6a",
|
||||
"AccountSid": "AC78349f4b974f5f6907c513a77d96756d",
|
||||
"From": "+491710000000",
|
||||
"ApiVersion": "2010-04-01"
|
||||
}
|
21
test/data/twilio/inbound_sms2.json
Normal file
21
test/data/twilio/inbound_sms2.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"ToCountry": "DE",
|
||||
"ToState": "",
|
||||
"SmsMessageSid": "SM1bd5330f81336e8b54c2d3d9fc1bcb6b",
|
||||
"NumMedia": "0",
|
||||
"ToCity": "",
|
||||
"FromZip": "",
|
||||
"SmsSid": "SM1bd5330f81336e8b54c2d3d9fc1bcb6b",
|
||||
"FromState": "",
|
||||
"SmsStatus": "received",
|
||||
"FromCity": "",
|
||||
"Body": "Follow up",
|
||||
"FromCountry": "DE",
|
||||
"To": "+4915700000000",
|
||||
"ToZip": "",
|
||||
"NumSegments": "1",
|
||||
"MessageSid": "SM1bd5330f81336e8b54c2d3d9fc1bcb6b",
|
||||
"AccountSid": "AC78349f4b974f5f6907c513a77d96756d",
|
||||
"From": "+491710000000",
|
||||
"ApiVersion": "2010-04-01"
|
||||
}
|
21
test/data/twilio/inbound_sms3.json
Normal file
21
test/data/twilio/inbound_sms3.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"ToCountry": "DE",
|
||||
"ToState": "",
|
||||
"SmsMessageSid": "SM1bd5330f81336e8b54c2d3d9fc1bcb6c",
|
||||
"NumMedia": "0",
|
||||
"ToCity": "",
|
||||
"FromZip": "",
|
||||
"SmsSid": "SM1bd5330f81336e8b54c2d3d9fc1bcb6c",
|
||||
"FromState": "",
|
||||
"SmsStatus": "received",
|
||||
"FromCity": "",
|
||||
"Body": "new 2",
|
||||
"FromCountry": "DE",
|
||||
"To": "+4915700000000",
|
||||
"ToZip": "",
|
||||
"NumSegments": "1",
|
||||
"MessageSid": "SM1bd5330f81336e8b54c2d3d9fc1bcb6c",
|
||||
"AccountSid": "AC78349f4b974f5f6907c513a77d96756d",
|
||||
"From": "+491710000000",
|
||||
"ApiVersion": "2010-04-01"
|
||||
}
|
19
test/fixtures/mail67.box
vendored
Normal file
19
test/fixtures/mail67.box
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
Return-Path: <info@example.de>
|
||||
X-Original-To: info@example.de
|
||||
Delivered-To: m032b9f7@dd38536.example.com
|
||||
Received: from dd38536.example.com (dd0801.example.com [1.1.1.1])
|
||||
by dd38536.example.com (Postfix) with ESMTPSA id 343463D42403
|
||||
for <info@example.de>; Wed, 4 Jul 2018 10:02:32 +0200 (CEST)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=ISO-8859-1
|
||||
Content-Transfer-Encoding: 8bit
|
||||
X-SenderIP: 1.1.1.1
|
||||
User-Agent: ALL-INKL Webmail 2.11
|
||||
Subject: Testmail - Alias in info@example.de Gruppe
|
||||
From: "Bob Smith | deal" <info@example.de>
|
||||
To: info@example.de
|
||||
Message-Id: <20180704080232.343463D42403@dd38536.example.com>
|
||||
Date: Wed, 4 Jul 2018 10:02:32 +0200 (CEST)
|
||||
X-KasLoop: m032b9f7
|
||||
|
||||
|
232
test/integration/twilio_sms_controller_test.rb
Normal file
232
test/integration/twilio_sms_controller_test.rb
Normal file
|
@ -0,0 +1,232 @@
|
|||
require 'test_helper'
|
||||
require 'rexml/document'
|
||||
require 'webmock/minitest'
|
||||
|
||||
class TwilioSmsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' }
|
||||
end
|
||||
|
||||
test 'basic call' do
|
||||
|
||||
# configure twilio channel
|
||||
bot_id = 123_456_789
|
||||
group_id = Group.find_by(name: 'Users').id
|
||||
|
||||
UserInfo.current_user_id = 1
|
||||
channel = Channel.create!(
|
||||
area: 'Sms::Account',
|
||||
options: {
|
||||
adapter: 'sms/twilio',
|
||||
webhook_token: 'f409460e50f76d331fdac8ba7b7963b6',
|
||||
account_id: '111',
|
||||
token: '223',
|
||||
sender: '333',
|
||||
},
|
||||
group_id: nil,
|
||||
active: true,
|
||||
)
|
||||
|
||||
# create agent
|
||||
agent = User.create!(
|
||||
login: 'tickets-agent@example.com',
|
||||
firstname: 'Tickets',
|
||||
lastname: 'Agent',
|
||||
email: 'tickets-agent@example.com',
|
||||
password: 'agentpw',
|
||||
active: true,
|
||||
roles: Role.where(name: 'Agent'),
|
||||
groups: Group.all,
|
||||
)
|
||||
|
||||
# process inbound sms
|
||||
post '/api/v1/sms_webhook', params: read_messaage('inbound_sms1'), headers: @headers
|
||||
assert_response(404)
|
||||
result = JSON.parse(@response.body)
|
||||
|
||||
post '/api/v1/sms_webhook/not_existing', params: read_messaage('inbound_sms1'), headers: @headers
|
||||
assert_response(404)
|
||||
result = JSON.parse(@response.body)
|
||||
|
||||
post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_messaage('inbound_sms1'), headers: @headers
|
||||
assert_response(422)
|
||||
result = JSON.parse(@response.body)
|
||||
assert_equal(result['error'], 'Can\'t use Channel::Driver::Sms::Twilio: #<Exceptions::UnprocessableEntity: Group needed in channel definition!>')
|
||||
|
||||
channel.group_id = Group.first.id
|
||||
channel.save!
|
||||
|
||||
post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_messaage('inbound_sms1'), headers: @headers
|
||||
assert_response(200)
|
||||
response = REXML::Document.new(@response.body)
|
||||
assert_equal(response.elements.count, 1)
|
||||
|
||||
ticket = Ticket.last
|
||||
article = Ticket::Article.last
|
||||
customer = User.last
|
||||
assert_equal(1, ticket.articles.count)
|
||||
assert_equal('Ldfhxhcuffufuf. Fifififig. Fifififiif F...', ticket.title)
|
||||
assert_equal('new', ticket.state.name)
|
||||
assert_equal(group_id, ticket.group_id)
|
||||
assert_equal(customer.id, ticket.customer_id)
|
||||
assert_equal(customer.id, ticket.created_by_id)
|
||||
assert_equal('+491710000000', article.from)
|
||||
assert_equal('+4915700000000', article.to)
|
||||
assert_nil(article.cc)
|
||||
assert_nil(article.subject)
|
||||
assert_equal('Ldfhxhcuffufuf. Fifififig. Fifififiif Fifififiif Fifififiif Fifififiif Fifififiif', article.body)
|
||||
assert_equal(customer.id, article.created_by_id)
|
||||
assert_equal('Customer', article.sender.name)
|
||||
assert_equal('sms', article.type.name)
|
||||
|
||||
post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_messaage('inbound_sms2'), headers: @headers
|
||||
assert_response(200)
|
||||
response = REXML::Document.new(@response.body)
|
||||
assert_equal(response.elements.count, 1)
|
||||
|
||||
ticket.reload
|
||||
assert_equal(2, ticket.articles.count)
|
||||
assert_equal('new', ticket.state.name)
|
||||
|
||||
article = Ticket::Article.last
|
||||
assert_equal('+491710000000', article.from)
|
||||
assert_equal('+4915700000000', article.to)
|
||||
assert_nil(article.cc)
|
||||
assert_nil(article.subject)
|
||||
assert_equal('Follow up', article.body)
|
||||
assert_equal('Customer', article.sender.name)
|
||||
assert_equal('sms', article.type.name)
|
||||
assert_equal(customer.id, article.created_by_id)
|
||||
|
||||
# check duplicate callbacks
|
||||
post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_messaage('inbound_sms2'), headers: @headers
|
||||
assert_response(200)
|
||||
response = REXML::Document.new(@response.body)
|
||||
assert_equal(response.elements.count, 1)
|
||||
|
||||
ticket.reload
|
||||
assert_equal(2, ticket.articles.count)
|
||||
assert_equal('new', ticket.state.name)
|
||||
assert_equal(Ticket::Article.last.id, article.id)
|
||||
|
||||
# new ticket need to be create
|
||||
ticket.state = Ticket::State.find_by(name: 'closed')
|
||||
ticket.save!
|
||||
|
||||
post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_messaage('inbound_sms3'), headers: @headers
|
||||
assert_response(200)
|
||||
response = REXML::Document.new(@response.body)
|
||||
assert_equal(response.elements.count, 1)
|
||||
|
||||
ticket.reload
|
||||
assert_equal(2, ticket.articles.count)
|
||||
assert_not_equal(Ticket.last.id, ticket.id)
|
||||
assert_equal('closed', ticket.state.name)
|
||||
|
||||
ticket = Ticket.last
|
||||
article = Ticket::Article.last
|
||||
customer = User.last
|
||||
assert_equal(1, ticket.articles.count)
|
||||
assert_equal('new 2', ticket.title)
|
||||
assert_equal(group_id, ticket.group_id)
|
||||
assert_equal(customer.id, ticket.customer_id)
|
||||
assert_equal(customer.id, ticket.created_by_id)
|
||||
assert_equal('+491710000000', article.from)
|
||||
assert_equal('+4915700000000', article.to)
|
||||
assert_nil(article.cc)
|
||||
assert_nil(article.subject)
|
||||
assert_equal('new 2', article.body)
|
||||
assert_equal(customer.id, article.created_by_id)
|
||||
assert_equal('Customer', article.sender.name)
|
||||
assert_equal('sms', article.type.name)
|
||||
|
||||
# reply by agent
|
||||
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-agent@example.com', 'agentpw')
|
||||
params = {
|
||||
ticket_id: ticket.id,
|
||||
body: 'some test',
|
||||
type: 'sms',
|
||||
}
|
||||
post '/api/v1/ticket_articles', params: params.to_json, headers: @headers.merge('Authorization' => credentials)
|
||||
assert_response(201)
|
||||
result = JSON.parse(@response.body)
|
||||
assert_equal(Hash, result.class)
|
||||
assert_nil(result['subject'])
|
||||
assert_equal('some test', result['body'])
|
||||
assert_equal('text/plain', result['content_type'])
|
||||
assert_equal(agent.id, result['updated_by_id'])
|
||||
assert_equal(agent.id, result['created_by_id'])
|
||||
|
||||
stub_request(:post, 'https://api.twilio.com/2010-04-01/Accounts/111/Messages.json')
|
||||
.with(
|
||||
body: {
|
||||
'Body' => 'some test',
|
||||
'From' => '333',
|
||||
'To' => nil,
|
||||
},
|
||||
headers: {
|
||||
'Accept' => 'application/json',
|
||||
'Accept-Charset' => 'utf-8',
|
||||
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
||||
'Authorization' => 'Basic MTExOjIyMw==',
|
||||
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
'User-Agent' => 'twilio-ruby/5.10.2 (ruby/x86_64-darwin16 2.4.4-p296)'
|
||||
}
|
||||
).to_return(status: 200, body: '', headers: {})
|
||||
|
||||
assert_nil(article.preferences[:delivery_retry])
|
||||
assert_nil(article.preferences[:delivery_status])
|
||||
|
||||
Observer::Transaction.commit
|
||||
Scheduler.worker(true)
|
||||
|
||||
article = Ticket::Article.find(result['id'])
|
||||
assert_equal(1, article.preferences[:delivery_retry])
|
||||
assert_equal('success', article.preferences[:delivery_status])
|
||||
|
||||
end
|
||||
|
||||
test 'customer based on already existing mobile attibute' do
|
||||
|
||||
customer = User.create!(
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
email: 'me@example.com',
|
||||
mobile: '01710000000',
|
||||
note: '',
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
Observer::Transaction.commit
|
||||
Scheduler.worker(true)
|
||||
|
||||
# configure twilio channel
|
||||
bot_id = 123_456_789
|
||||
group_id = Group.find_by(name: 'Users').id
|
||||
|
||||
UserInfo.current_user_id = 1
|
||||
channel = Channel.create!(
|
||||
area: 'Sms::Account',
|
||||
options: {
|
||||
adapter: 'sms/twilio',
|
||||
webhook_token: 'f409460e50f76d331fdac8ba7b7963b6',
|
||||
account_id: '111',
|
||||
token: '223',
|
||||
sender: '333',
|
||||
},
|
||||
group_id: group_id,
|
||||
active: true,
|
||||
)
|
||||
|
||||
post '/api/v1/sms_webhook/f409460e50f76d331fdac8ba7b7963b6', params: read_messaage('inbound_sms1'), headers: @headers
|
||||
assert_response(200)
|
||||
response = REXML::Document.new(@response.body)
|
||||
assert_equal(response.elements.count, 1)
|
||||
|
||||
assert_equal(User.last.id, customer.id)
|
||||
end
|
||||
|
||||
def read_messaage(file)
|
||||
File.read(Rails.root.join('test', 'data', 'twilio', "#{file}.json"))
|
||||
end
|
||||
end
|
|
@ -583,4 +583,56 @@ class NotificationFactoryRendererTest < ActiveSupport::TestCase
|
|||
|
||||
end
|
||||
|
||||
test 'methods with single Integer parameter' do
|
||||
|
||||
template = "\#{ticket.title.first(3)}"
|
||||
result = described_class.new(
|
||||
{
|
||||
ticket: ticket,
|
||||
},
|
||||
'en-us',
|
||||
template,
|
||||
).render
|
||||
assert_equal(CGI.escapeHTML('<b>'), result)
|
||||
|
||||
template = "\#{ticket.title.last(4)}"
|
||||
result = described_class.new(
|
||||
{
|
||||
ticket: ticket,
|
||||
},
|
||||
'en-us',
|
||||
template,
|
||||
).render
|
||||
assert_equal(CGI.escapeHTML('</b>'), result)
|
||||
|
||||
template = "\#{ticket.title.slice(3, 4)}"
|
||||
result = described_class.new(
|
||||
{
|
||||
ticket: ticket,
|
||||
},
|
||||
'en-us',
|
||||
template,
|
||||
).render
|
||||
assert_equal(CGI.escapeHTML("\#{ticket.title.slice(3,4) / invalid parameter: 3,4}"), result)
|
||||
|
||||
template = "\#{ticket.title.first('some invalid parameter')}"
|
||||
result = described_class.new(
|
||||
{
|
||||
ticket: ticket,
|
||||
},
|
||||
'en-us',
|
||||
template,
|
||||
).render
|
||||
assert_equal("\#{ticket.title.first(someinvalidparameter) / invalid parameter: someinvalidparameter}", result)
|
||||
|
||||
template = "\#{ticket.title.chomp(`cat /etc/passwd`)}"
|
||||
result = described_class.new(
|
||||
{
|
||||
ticket: ticket,
|
||||
},
|
||||
'en-us',
|
||||
template,
|
||||
).render
|
||||
assert_equal("\#{ticket.title.chomp(`cat/etc/passwd`) / not allowed}", result)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue