Added slack integration.

This commit is contained in:
Martin Edenhofer 2016-04-15 23:56:10 +02:00
parent 74d47deeea
commit a102057506
48 changed files with 1403 additions and 309 deletions

View file

@ -88,6 +88,9 @@ gem 'writeexcel'
gem 'icalendar' gem 'icalendar'
gem 'browser' gem 'browser'
# integrations
gem 'slack-notifier'
# event machine # event machine
gem 'eventmachine' gem 'eventmachine'
gem 'em-websocket' gem 'em-websocket'

View file

@ -267,6 +267,7 @@ GEM
simplecov-html (0.10.0) simplecov-html (0.10.0)
simplecov-rcov (0.2.3) simplecov-rcov (0.2.3)
simplecov (>= 0.4.1) simplecov (>= 0.4.1)
slack-notifier (1.5.1)
slop (3.6.0) slop (3.6.0)
spring (1.6.4) spring (1.6.4)
sprockets (3.5.2) sprockets (3.5.2)
@ -361,6 +362,7 @@ DEPENDENCIES
simple-rss simple-rss
simplecov simplecov
simplecov-rcov simplecov-rcov
slack-notifier
spring spring
sprockets sprockets
sqlite3 sqlite3

View file

@ -1,12 +1,30 @@
class Icinga extends App.ControllerTabs class Index extends App.ControllerIntegrationBase
header: 'Icinga' featureIntegration: 'icinga_integration'
constructor: -> featureName: 'Icinga'
super featureConfig: 'icinga_config'
return if !@authenticate(false, 'Admin') description: [
@title 'Icinga', true ['This service receives emails from %s and creates tickets with host and service.', 'Icinga']
@tabs = [ ['If the host and service is recovered again, the ticket will be closed automatically.']
{ name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Integration::Icinga' } }
] ]
@render()
App.Config.set('IntegrationIcinga', { prio: 1100, parent: '#integration', name: 'Icinga', target: '#integration/icinga', controller: Icinga, role: ['Admin'] }, 'NavBarIntegration') form: (localeEl) ->
new App.SettingsForm(
area: 'Integration::Icinga'
el: localeEl.find('.js-form')
)
class State
@current: ->
App.Setting.get('icinga_integration')
App.Config.set(
'IntegrationIcinga'
{
name: 'Icinga'
target: '#system/integration/icinga'
description: 'A open source monitoring tool.'
controller: Index
state: State
}
'NavBarIntegrations'
)

View file

@ -1,12 +0,0 @@
class Mattermost extends App.ControllerTabs
header: 'Mattermost'
constructor: ->
super
return if !@authenticate(false, 'Admin')
@title 'Mattermost', true
@tabs = [
{ name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Integration::Mattermost' } }
]
@render()
App.Config.set('IntegrationMattermost', { prio: 1000, parent: '#integration', name: 'Mattermost', target: '#integration/mattermost', controller: Mattermost, role: ['Admin'] }, 'NavBarIntegration')

View file

@ -1,12 +1,30 @@
class Nagios extends App.ControllerTabs class Index extends App.ControllerIntegrationBase
header: 'Nagios' featureIntegration: 'nagios_integration'
constructor: -> featureName: 'Nagios'
super featureConfig: 'nagios_config'
return if !@authenticate(false, 'Admin') description: [
@title 'Nagios', true ['This service receives emails from %s and creates tickets with host and service.', 'Nagios']
@tabs = [ ['If the host and service is recovered again, the ticket will be closed automatically.']
{ name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Integration::Nagios' } }
] ]
@render()
App.Config.set('IntegrationNagios', { prio: 1200, parent: '#integration', name: 'Nagios', target: '#integration/nagios', controller: Nagios, role: ['Admin'] }, 'NavBarIntegration') form: (localeEl) ->
new App.SettingsForm(
area: 'Integration::Nagios'
el: localeEl.find('.js-form')
)
class State
@current: ->
App.Setting.get('nagios_integration')
App.Config.set(
'IntegrationNagios'
{
name: 'Nagios'
target: '#system/integration/nagios'
description: 'A open source monitoring tool.'
controller: Index
state: State
}
'NavBarIntegrations'
)

View file

@ -0,0 +1,73 @@
class Index extends App.ControllerIntegrationBase
featureIntegration: 'slack_integration'
featureName: 'Slack'
featureConfig: 'slack_config'
description: [
['This service sends notifications to your %s channel.', 'Slack']
['To setup this Service you need to create a new |"Incoming webhook"| in your %s integration panel, and enter the Webhook URL below.', 'Slack']
]
form: (localEl) =>
params = App.Setting.get(@featureConfig)
if params && params.items
params = params.items[0] || {}
options =
create: '1. Ticket Create'
update: '2. Ticket Update'
reminder_reached: '3. Ticket Reminder Reached'
escalation: '4. Ticket Escalation'
escalation_warning: '5. Ticket Escalation Warning'
configureAttributes = [
{ name: 'types', display: 'Trigger', tag: 'checkbox', options: options, 'null': false, class: 'vertical', note: 'Where notification is sent.' },
{ name: 'group_id', display: 'Group', tag: 'select', relation: 'Group', multiple: true, 'null': false, note: 'Only for this groups.' },
{ name: 'webhook', display: 'Webhook', tag: 'input', type: 'text', limit: 200, 'null': false, placeholder: 'https://hooks.slack.com/services/...' },
{ name: 'username', display: 'username', tag: 'input', type: 'text', limit: 100, 'null': false, placeholder: 'username' },
{ name: 'channel', display: 'channel', tag: 'input', type: 'text', limit: 100, 'null': true, placeholder: '#channel' },
]
console.log('p', params)
settings = []
for item in configureAttributes
setting =
options:
form: [item]
name: item.name
description: item.note || ''
title: item.display
settings.push setting
formEl = $( App.view('settings/form')(
settings: settings
))
for setting in settings
configure_attribute = setting.options['form']
configure_attribute[0].display = ''
value = params[setting.name]
localParams = {}
localParams[setting.name] = value
new App.ControllerForm(
el: formEl.find("[data-name=#{setting.name}]")
model: { configure_attributes: configure_attribute, className: '' }
params: localParams
)
localEl.find('.js-form').html(formEl)
class State
@current: ->
App.Setting.get('slack_integration')
App.Config.set(
'IntegrationSlack'
{
name: 'Slack'
target: '#system/integration/slack'
description: 'A team communication tool for the 21st century.'
controller: Index
state: State
}
'NavBarIntegrations'
)

View file

@ -37,7 +37,7 @@ class Index extends App.Controller
} }
auth_providers = [] auth_providers = []
for key, provider of auth_provider_all for key, provider of auth_provider_all
if @Config.get( provider.config ) is true || @Config.get( provider.config ) is 'true' if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true'
auth_providers.push provider auth_providers.push provider
@html App.view('profile/linked_accounts')( @html App.view('profile/linked_accounts')(
@ -55,7 +55,7 @@ class Index extends App.Controller
id: 'account' id: 'account'
type: 'DELETE' type: 'DELETE'
url: @apiPath + '/users/account' url: @apiPath + '/users/account'
data: JSON.stringify({ provider: provider, uid: uid }) data: JSON.stringify(provider: provider, uid: uid)
processData: true processData: true
success: @success success: @success
error: @error error: @error
@ -64,7 +64,7 @@ class Index extends App.Controller
success: (data, status, xhr) => success: (data, status, xhr) =>
@notify( @notify(
type: 'success' type: 'success'
msg: App.i18n.translateContent( 'Successfully!' ) msg: App.i18n.translateContent('Successfully!')
) )
update = => update = =>
@render() @render()
@ -72,10 +72,10 @@ class Index extends App.Controller
error: (xhr, status, error) => error: (xhr, status, error) =>
@render() @render()
data = JSON.parse( xhr.responseText ) data = JSON.parse(xhr.responseText)
@notify( @notify(
type: 'error' type: 'error'
msg: App.i18n.translateContent( data.message ) msg: App.i18n.translateContent(data.message)
) )
App.Config.set( 'LinkedAccounts', { prio: 4000, name: 'Linked Accounts', parent: '#profile', target: '#profile/linked', controller: Index }, 'NavBarProfile' ) App.Config.set('LinkedAccounts', { prio: 4000, name: 'Linked Accounts', parent: '#profile', target: '#profile/linked', controller: Index }, 'NavBarProfile')

View file

@ -1,3 +1,93 @@
class App.SettingsForm extends App.Controller
events:
'submit form': 'update'
constructor: ->
super
# check authentication
return if !@authenticate()
@subscribeId = App.Setting.subscribe(@render, initFetch: true, clear: false)
render: =>
# serach area settings
settings = App.Setting.search(
filter:
area: @area
)
# filter online service settings
if App.Config.get('system_online_service')
settings = _.filter(settings, (setting) ->
return if setting.online_service
return if setting.preferences && setting.preferences.online_service_disable
setting
)
return if _.isEmpty(settings)
# sort by prio
settings = _.sortBy( settings, (setting) ->
return if !setting.preferences
setting.preferences.prio
)
localEl = $( App.view('settings/form')(
settings: settings
))
for setting in settings
configure_attributes = setting.options['form']
value = App.Setting.get(setting.name)
params = {}
params[setting.name] = value
new App.ControllerForm(
el: localEl.find("[data-name=#{setting.name}]")
model: { configure_attributes: configure_attributes, className: '' }
params: params
)
@html localEl
update: (e) =>
e.preventDefault()
@formDisable(e)
params = @formParam(e.target)
ui = @
count = 0
for name, value of params
if App.Setting.findByAttribute('name', name)
count += 1
App.Setting.set(
name,
value,
done: ->
ui.formEnable(e)
count -= 1
if count == 0
App.Event.trigger 'notify', {
type: 'success'
msg: App.i18n.translateContent('Update successful!')
timeout: 2000
}
# rerender ui || get new collections and session data
if @preferences
if @preferences.render
App.Event.trigger( 'ui:rerender' )
if @preferences.session_check
App.Auth.loginCheck()
fail: (settings, details) ->
App.Event.trigger 'notify', {
type: 'error'
msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to update object!')
timeout: 2000
}
)
class App.SettingsArea extends App.Controller class App.SettingsArea extends App.Controller
constructor: -> constructor: ->
super super
@ -115,11 +205,11 @@ class App.SettingsAreaItem extends App.Controller
if @setting.preferences.session_check if @setting.preferences.session_check
App.Auth.loginCheck() App.Auth.loginCheck()
fail: -> fail: (settings, details) ->
ui.formEnable(e) ui.formEnable(e)
App.Event.trigger 'notify', { App.Event.trigger 'notify', {
type: 'error' type: 'error'
msg: App.i18n.translateContent('Can\'t update item!') msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to update object!')
timeout: 2000 timeout: 2000
} }
) )

View file

@ -1,8 +1,41 @@
class IndexRouter extends App.ControllerNavSidbar class Index extends App.ControllerContent
authenticateRequired: true constructor: ->
configKey: 'NavBarIntegration' super
App.Config.set('integration', IndexRouter, 'Routes') # check authentication
App.Config.set('integration/:target', IndexRouter, 'Routes') return if !@authenticate(false, 'Admin')
App.Config.set('Integration', { prio: 1000, name: 'Integration', target: '#integration', role: ['Admin'] }, 'NavBarIntegration') @title 'Integrations', true
@integrationItems = App.Config.get('NavBarIntegrations')
if !@integration
@subscribeId = App.Setting.subscribe(@render, initFetch: true, clear: false)
return
for key, value of @integrationItems
if value.target is "#system/#{@target}/#{@integration}"
config = value
break
new config.controller(
el: @el.closest('.main')
)
render: =>
integrations = []
for key, value of @integrationItems
value.key = key
integrations.push value
integrations = _.sortBy(integrations, (item) -> return item.name)
@html App.view('integration/index')(
head: 'Integrations'
integrations: integrations
)
release: =>
if @subscribeId
App.Setting.unsubscribe(@subscribeId)
App.Config.set('Integration', { prio: 1000, name: 'Integrations', parent: '#system', target: '#system/integration', controller: Index, role: ['Admin'] }, 'NavBarAdmin')

View file

@ -8,6 +8,7 @@ App.Config.set('settings/:target', IndexRouter, 'Routes')
App.Config.set('channels/:target', IndexRouter, 'Routes') App.Config.set('channels/:target', IndexRouter, 'Routes')
App.Config.set('channels/:target/:channel_id', IndexRouter, 'Routes') App.Config.set('channels/:target/:channel_id', IndexRouter, 'Routes')
App.Config.set('system/:target', IndexRouter, 'Routes') App.Config.set('system/:target', IndexRouter, 'Routes')
App.Config.set('system/:target/:integration', IndexRouter, 'Routes')
App.Config.set('Manage', { prio: 1000, name: 'Manage', target: '#manage', role: ['Admin'] }, 'NavBarAdmin') App.Config.set('Manage', { prio: 1000, name: 'Manage', target: '#manage', role: ['Admin'] }, 'NavBarAdmin')
App.Config.set('Channels', { prio: 2500, name: 'Channels', target: '#channels', role: ['Admin'] }, 'NavBarAdmin') App.Config.set('Channels', { prio: 2500, name: 'Channels', target: '#channels', role: ['Admin'] }, 'NavBarAdmin')

View file

@ -58,4 +58,4 @@ class Index extends App.ControllerContent
@load() @load()
) )
App.Config.set( 'Packages', { prio: 1000, name: 'Packages', parent: '#system', target: '#system/package', controller: Index, role: ['Admin'] }, 'NavBarAdmin' ) App.Config.set('Packages', { prio: 1000, name: 'Packages', parent: '#system', target: '#system/package', controller: Index, role: ['Admin'] }, 'NavBarAdmin')

View file

@ -2,3 +2,13 @@ class App.Setting extends App.Model
@configure 'Setting', 'name', 'state_current' @configure 'Setting', 'name', 'state_current'
@extend Spine.Model.Ajax @extend Spine.Model.Ajax
@url: @apiPath + '/settings' @url: @apiPath + '/settings'
@get: (name) ->
setting = App.Setting.findByAttribute('name', name)
setting.state_current.value
@set: (name, value, options = {}) ->
setting = App.Setting.findByAttribute('name', name)
setting.state_current.value = value
setting.save(options)
App.Config.set(name, value)

View file

@ -0,0 +1,17 @@
<div class="page-header">
<div class="page-header-title">
<div class="zammad-switch zammad-switch--small js-switch">
<input name="<%- @feature %>" type="checkbox" id="setting-switch" <% if @featureEnabled: %>checked<% end %>>
<label for="setting-switch"></label>
</div>
<h1><%- @T(@header) %></h1>
</div>
</div>
<div class="page-content">
<% if @description: %>
<% for item in @description: %>
<p><%- @T(item[0], item[1], item[2]) %></p>
<% end %>
<% end %>
<div class="js-form"></div>
</div>

View file

@ -0,0 +1,29 @@
<div class="page-header-title">
<h1><%- @T(@head) %></h1>
</div>
<div class="page-content">
<table class="table table-striped table-hover">
<thead>
<tr>
<th style="width: 30px;"></th>
<th style="width: 40%;"><%- @T('Service') %></th>
<th><%- @T('Description') %></th>
</tr>
</thead>
<tbody>
<% for integration in @integrations: %>
<tr data-key="<%= integration.key %>">
<td>
<% if !integration.state.current(): %>
<%- @Icon('status', 'inactive inline') %>
<% else: %>
<%- @Icon('status', 'ok inline') %>
<% end %>
</td>
<td><a href="<%- integration.target %>"><%= integration.name %></a></td>
<td><%= integration.description %></td>
</tr>
<% end %>
</tbody>
</table>
</div>

View file

@ -0,0 +1,21 @@
<form>
<div class="settings-entry">
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th width="15%"><%- @T('Title') %>
<th width="50%"><%- @T('Value') %>
<th width="35%"><%- @T('Description') %>
</thead>
<tbody>
<% for setting in @settings: %>
<tr>
<td><%- @T(setting.title) %>
<td data-name="<%- setting.name %>">
<td><p class="help-text"><%- @RichText(setting.description) %></p>
<% end %>
</tbody>
</table>
</div>
<button type="submit" class="btn btn--primary"><%- @T('Submit') %></button>
</form>

View file

@ -1,8 +1,8 @@
<form class="settings-entry" id="<%= @setting.name %>"> <form class="settings-entry" id="<%= @setting.name %>">
<h2><%- @T( @setting.title ) %></h2> <h2><%- @T(@setting.title) %></h2>
<p class="help-text"><%- @RichText( @setting.description ) %></p> <p class="help-text"><%- @RichText(@setting.description) %></p>
<div class="horizontal end"> <div class="horizontal end">
<div class="form-item flex"></div> <div class="form-item flex"></div>
<button type="submit" class="btn btn--primary"><%- @T( 'Submit' ) %></button> <button type="submit" class="btn btn--primary"><%- @T('Submit') %></button>
</div> </div>
</form> </form>

View file

@ -6962,6 +6962,7 @@ output {
} }
th, td { th, td {
vertical-align: top;
padding: 10px; padding: 10px;
border: 1px solid hsl(198,18%,86%); border: 1px solid hsl(198,18%,86%);
} }

View file

@ -1,180 +0,0 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
require 'event_buffer'
require 'notification_factory'
class Observer::Ticket::Notification < ActiveRecord::Observer
observe :ticket, 'ticket::_article'
def self.transaction(params)
return if params[:disable_notification]
# return if we run import mode
return if Setting.get('import_mode')
# get buffer
list = EventBuffer.list('notification')
# reset buffer
EventBuffer.reset('notification')
via_web = false
if ENV['RACK_ENV'] || Rails.configuration.webserver_is_active
via_web = true
end
# get uniq objects
list_objects = get_uniq_changes(list)
list_objects.each {|_ticket_id, item|
# send background job
Delayed::Job.enqueue(Observer::Ticket::Notification::BackgroundJob.new(item, via_web))
}
end
=begin
result = get_uniq_changes(events)
result = {
1 => {
type: 'create',
ticket_id: 123,
article_id: 123,
},
9 => {
type: 'update',
ticket_id: 123,
changes: {
attribute1: [before, now],
attribute2: [before, now],
}
},
}
result = {
9 => {
type: 'update',
ticket_id: 123,
article_id: 123,
changes: {
attribute1: [before, now],
attribute2: [before, now],
}
},
}
=end
def self.get_uniq_changes(events)
list_objects = {}
events.each { |event|
# get current state of objects
if event[:name] == 'Ticket::Article'
article = Ticket::Article.lookup(id: event[:id])
# next if article is already deleted
next if !article
ticket = article.ticket
if !list_objects[ticket.id]
list_objects[ticket.id] = {}
end
list_objects[ticket.id][:article_id] = article.id
list_objects[ticket.id][:ticket_id] = ticket.id
if !list_objects[ticket.id][:type]
list_objects[ticket.id][:type] = 'update'
end
elsif event[:name] == 'Ticket'
ticket = Ticket.lookup(id: event[:id])
# next if ticket is already deleted
next if !ticket
if !list_objects[ticket.id]
list_objects[ticket.id] = {}
end
list_objects[ticket.id][:ticket_id] = ticket.id
if !list_objects[ticket.id][:type] || list_objects[ticket.id][:type] == 'update'
list_objects[ticket.id][:type] = event[:type]
end
# merge changes
if event[:changes]
if !list_objects[ticket.id][:changes]
list_objects[ticket.id][:changes] = event[:changes]
else
event[:changes].each {|key, value|
if !list_objects[ticket.id][:changes][key]
list_objects[ticket.id][:changes][key] = value
else
list_objects[ticket.id][:changes][key][1] = value[1]
end
}
end
end
else
raise "unknown object for notification #{event[:name]}"
end
}
list_objects
end
def after_create(record)
# return if we run import mode
return if Setting.get('import_mode')
# Rails.logger.info 'CREATED!!!!'
# Rails.logger.info record.inspect
e = {
name: record.class.name,
type: 'create',
data: record,
id: record.id,
}
EventBuffer.add('notification', e)
end
def before_update(record)
# return if we run import mode
return if Setting.get('import_mode')
# ignore updates on articles / we just want send notifications on ticket updates
return if record.class.name == 'Ticket::Article'
# ignore certain attributes
real_changes = {}
record.changes.each {|key, value|
next if key == 'updated_at'
next if key == 'first_response'
next if key == 'close_time'
next if key == 'last_contact_agent'
next if key == 'last_contact_customer'
next if key == 'last_contact'
next if key == 'article_count'
next if key == 'create_article_type_id'
next if key == 'create_article_sender_id'
real_changes[key] = value
}
# do not send anything if nothing has changed
return if real_changes.empty?
e = {
name: record.class.name,
type: 'update',
data: record,
changes: real_changes,
id: record.id,
}
EventBuffer.add('notification', e)
end
end

View file

@ -1,10 +1,185 @@
class Observer::Transaction # Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
class Observer::Transaction < ActiveRecord::Observer
observe :ticket, 'ticket::_article'
def self.commit(params = {}) def self.commit(params = {})
# execute ticket transactions # add attribute if execution is via web
Observer::Ticket::Notification.transaction(params) params[:via_web] = false
if ENV['RACK_ENV'] || Rails.configuration.webserver_is_active
params[:via_web] = true
end
# execute object transactions
Observer::Transaction.perform(params)
end
def self.perform(params)
# return if we run import mode
return if Setting.get('import_mode')
# get buffer
list = EventBuffer.list('transaction')
# reset buffer
EventBuffer.reset('transaction')
# get uniq objects
list_objects = get_uniq_changes(list)
list_objects.each {|_id, item|
# send background job
Delayed::Job.enqueue(Transaction::BackgroundJob.new(item, params))
}
end
=begin
result = get_uniq_changes(events)
result = {
1 => {
object: 'Ticket',
type: 'create',
ticket_id: 123,
article_id: 123,
},
9 => {
object: 'Ticket',
type: 'update',
ticket_id: 123,
changes: {
attribute1: [before, now],
attribute2: [before, now],
}
},
}
result = {
9 => {
object: 'Ticket',
type: 'update',
ticket_id: 123,
article_id: 123,
changes: {
attribute1: [before, now],
attribute2: [before, now],
}
},
}
=end
def self.get_uniq_changes(events)
list_objects = {}
events.each { |event|
# get current state of objects
if event[:name] == 'Ticket::Article'
article = Ticket::Article.lookup(id: event[:id])
# next if article is already deleted
next if !article
ticket = article.ticket
if !list_objects[ticket.id]
list_objects[ticket.id] = {}
end
list_objects[ticket.id][:object] = 'Ticket'
list_objects[ticket.id][:article_id] = article.id
list_objects[ticket.id][:ticket_id] = ticket.id
if !list_objects[ticket.id][:type]
list_objects[ticket.id][:type] = 'update'
end
elsif event[:name] == 'Ticket'
ticket = Ticket.lookup(id: event[:id])
# next if ticket is already deleted
next if !ticket
if !list_objects[ticket.id]
list_objects[ticket.id] = {}
end
list_objects[ticket.id][:object] = 'Ticket'
list_objects[ticket.id][:ticket_id] = ticket.id
if !list_objects[ticket.id][:type] || list_objects[ticket.id][:type] == 'update'
list_objects[ticket.id][:type] = event[:type]
end
# merge changes
if event[:changes]
if !list_objects[ticket.id][:changes]
list_objects[ticket.id][:changes] = event[:changes]
else
event[:changes].each {|key, value|
if !list_objects[ticket.id][:changes][key]
list_objects[ticket.id][:changes][key] = value
else
list_objects[ticket.id][:changes][key][1] = value[1]
end
}
end
end
else
raise "unknown object for integration #{event[:name]}"
end
}
list_objects
end
def after_create(record)
# return if we run import mode
return if Setting.get('import_mode')
e = {
name: record.class.name,
type: 'create',
data: record,
id: record.id,
}
EventBuffer.add('transaction', e)
end
def before_update(record)
# return if we run import mode
return if Setting.get('import_mode')
# ignore updates on articles / we just want send integrations on ticket updates
return if record.class.name == 'Ticket::Article'
# ignore certain attributes
real_changes = {}
record.changes.each {|key, value|
next if key == 'updated_at'
next if key == 'first_response'
next if key == 'close_time'
next if key == 'last_contact_agent'
next if key == 'last_contact_customer'
next if key == 'last_contact'
next if key == 'article_count'
next if key == 'create_article_type_id'
next if key == 'create_article_sender_id'
real_changes[key] = value
}
# do not send anything if nothing has changed
return if real_changes.empty?
e = {
name: record.class.name,
type: 'update',
data: record,
changes: real_changes,
id: record.id,
}
EventBuffer.add('transaction', e)
end end
end end

View file

@ -180,12 +180,12 @@ returns
tickets.each { |ticket| tickets.each { |ticket|
# send notification # send notification
bg = Observer::Ticket::Notification::BackgroundJob.new( Transaction::BackgroundJob.run(
object: 'Ticket',
type: 'reminder_reached',
ticket_id: ticket.id, ticket_id: ticket.id,
article_id: ticket.articles.last.id, article_id: ticket.articles.last.id,
type: 'reminder_reached',
) )
bg.perform
result.push ticket result.push ticket
} }
@ -220,23 +220,23 @@ returns
# send escalation # send escalation
if ticket.escalation_time < Time.zone.now if ticket.escalation_time < Time.zone.now
bg = Observer::Ticket::Notification::BackgroundJob.new( Transaction::BackgroundJob.run(
object: 'Ticket',
type: 'escalation',
ticket_id: ticket.id, ticket_id: ticket.id,
article_id: ticket.articles.last.id, article_id: ticket.articles.last.id,
type: 'escalation',
) )
bg.perform
result.push ticket result.push ticket
next next
end end
# check if warning need to be sent # check if warning need to be sent
bg = Observer::Ticket::Notification::BackgroundJob.new( Transaction::BackgroundJob.run(
object: 'Ticket',
type: 'escalation_warning',
ticket_id: ticket.id, ticket_id: ticket.id,
article_id: ticket.articles.last.id, article_id: ticket.articles.last.id,
type: 'escalation_warning',
) )
bg.perform
result.push ticket result.push ticket
} }
result result

View file

@ -0,0 +1,3 @@
class Transaction
end

View file

@ -0,0 +1,36 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
class Transaction::BackgroundJob
def initialize(item, params = {})
=begin
{
object: 'Ticket',
type: 'update',
ticket_id: 123,
via_web: true,
changes: {
'attribute1' => [before,now],
'attribute2' => [before,now],
}
},
=end
@item = item
@params = params
end
def perform
Setting.where(area: 'Transaction::Backend').order(:name).each {|setting|
backend = Setting.get(setting.name)
integration = Kernel.const_get(backend).new(@item, @params)
integration.perform
}
end
def self.run(item, params = {})
generic = new(item, params)
generic.perform
end
end

View file

@ -1,24 +1,29 @@
# encoding: utf-8 # Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::Notification::BackgroundJob class Transaction::Notification
def initialize(params, via_web = false)
=begin =begin
{
object: 'Ticket',
type: 'update', type: 'update',
ticket_id: 123, ticket_id: 123,
via_web: true,
changes: { changes: {
'attribute1' => [before,now], 'attribute1' => [before, now],
'attribute2' => [before,now], 'attribute2' => [before, now],
} }
},
=end =end
@p = params
@via_web = via_web def initialize(item, params = {})
@item = item
@params = params
end end
def perform def perform
ticket = Ticket.find(@p[:ticket_id]) ticket = Ticket.find(@item[:ticket_id])
if @p[:article_id] if @item[:article_id]
article = Ticket::Article.find(@p[:article_id]) article = Ticket::Article.find(@item[:article_id])
end end
# find recipients # find recipients
@ -59,7 +64,7 @@ class Observer::Ticket::Notification::BackgroundJob
end end
already_checked_recipient_ids = {} already_checked_recipient_ids = {}
possible_recipients.each {|user| possible_recipients.each {|user|
result = NotificationFactory::Mailer.notification_settings(user, ticket, @p[:type]) result = NotificationFactory::Mailer.notification_settings(user, ticket, @item[:type])
next if !result next if !result
next if already_checked_recipient_ids[result[:user].id] next if already_checked_recipient_ids[result[:user].id]
already_checked_recipient_ids[result[:user].id] = true already_checked_recipient_ids[result[:user].id] = true
@ -73,7 +78,7 @@ class Observer::Ticket::Notification::BackgroundJob
channels = item[:channels] channels = item[:channels]
# ignore user who changed it by him self via web # ignore user who changed it by him self via web
if @via_web if @params[:via_web]
next if article && article.updated_by_id == user.id next if article && article.updated_by_id == user.id
next if !article && ticket.updated_by_id == user.id next if !article && ticket.updated_by_id == user.id
end end
@ -83,10 +88,10 @@ class Observer::Ticket::Notification::BackgroundJob
# ignore if no changes has been done # ignore if no changes has been done
changes = human_changes(user, ticket) changes = human_changes(user, ticket)
next if @p[:type] == 'update' && !article && (!changes || changes.empty?) next if @item[:type] == 'update' && !article && (!changes || changes.empty?)
# check if today already notified # check if today already notified
if @p[:type] == 'reminder_reached' || @p[:type] == 'escalation' || @p[:type] == 'escalation_warning' if @item[:type] == 'reminder_reached' || @item[:type] == 'escalation' || @item[:type] == 'escalation_warning'
identifier = user.email identifier = user.email
if !identifier || identifier == '' if !identifier || identifier == ''
identifier = user.login identifier = user.login
@ -94,7 +99,7 @@ class Observer::Ticket::Notification::BackgroundJob
already_notified = false already_notified = false
History.list('Ticket', ticket.id).each {|history| History.list('Ticket', ticket.id).each {|history|
next if history['type'] != 'notification' next if history['type'] != 'notification'
next if history['value_to'] !~ /\(#{Regexp.escape(@p[:type])}:/ next if history['value_to'] !~ /\(#{Regexp.escape(@item[:type])}:/
next if history['value_to'] !~ /#{Regexp.escape(identifier)}\(/ next if history['value_to'] !~ /#{Regexp.escape(identifier)}\(/
next if !history['created_at'].today? next if !history['created_at'].today?
already_notified = true already_notified = true
@ -110,59 +115,59 @@ class Observer::Ticket::Notification::BackgroundJob
created_by_id = ticket.updated_by_id || 1 created_by_id = ticket.updated_by_id || 1
# delete old notifications # delete old notifications
if @p[:type] == 'reminder_reached' if @item[:type] == 'reminder_reached'
seen = false seen = false
created_by_id = 1 created_by_id = 1
OnlineNotification.remove_by_type('Ticket', ticket.id, @p[:type], user) OnlineNotification.remove_by_type('Ticket', ticket.id, @item[:type], user)
elsif @p[:type] == 'escalation' || @p[:type] == 'escalation_warning' elsif @item[:type] == 'escalation' || @item[:type] == 'escalation_warning'
seen = false seen = false
created_by_id = 1 created_by_id = 1
OnlineNotification.remove_by_type('Ticket', ticket.id, 'escalation', user) OnlineNotification.remove_by_type('Ticket', ticket.id, 'escalation', user)
OnlineNotification.remove_by_type('Ticket', ticket.id, 'escalation_warning', user) OnlineNotification.remove_by_type('Ticket', ticket.id, 'escalation_warning', user)
# on updates without state changes create unseen messages # on updates without state changes create unseen messages
elsif @p[:type] != 'create' && (!@p[:changes] || @p[:changes].empty? || !@p[:changes]['state_id']) elsif @item[:type] != 'create' && (!@item[:changes] || @item[:changes].empty? || !@item[:changes]['state_id'])
seen = false seen = false
else else
seen = ticket.online_notification_seen_state(user.id) seen = ticket.online_notification_seen_state(user.id)
end end
OnlineNotification.add( OnlineNotification.add(
type: @p[:type], type: @item[:type],
object: 'Ticket', object: 'Ticket',
o_id: ticket.id, o_id: ticket.id,
seen: seen, seen: seen,
created_by_id: created_by_id, created_by_id: created_by_id,
user_id: user.id, user_id: user.id,
) )
Rails.logger.debug "sent ticket online notifiaction to agent (#{@p[:type]}/#{ticket.id}/#{user.email})" Rails.logger.debug "sent ticket online notifiaction to agent (#{@item[:type]}/#{ticket.id}/#{user.email})"
end end
# ignore email channel notificaiton and empty emails # ignore email channel notificaiton and empty emails
if !channels['email'] || !user.email || user.email == '' if !channels['email'] || !user.email || user.email == ''
add_recipient_list(ticket, user, used_channels, @p[:type]) add_recipient_list(ticket, user, used_channels, @item[:type])
next next
end end
used_channels.push 'email' used_channels.push 'email'
add_recipient_list(ticket, user, used_channels, @p[:type]) add_recipient_list(ticket, user, used_channels, @item[:type])
# get user based notification template # get user based notification template
# if create, send create message / block update messages # if create, send create message / block update messages
template = nil template = nil
if @p[:type] == 'create' if @item[:type] == 'create'
template = 'ticket_create' template = 'ticket_create'
elsif @p[:type] == 'update' elsif @item[:type] == 'update'
template = 'ticket_update' template = 'ticket_update'
elsif @p[:type] == 'reminder_reached' elsif @item[:type] == 'reminder_reached'
template = 'ticket_reminder_reached' template = 'ticket_reminder_reached'
elsif @p[:type] == 'escalation' elsif @item[:type] == 'escalation'
template = 'ticket_escalation' template = 'ticket_escalation'
elsif @p[:type] == 'escalation_warning' elsif @item[:type] == 'escalation_warning'
template = 'ticket_escalation_warning' template = 'ticket_escalation_warning'
else else
raise "unknown type for notification #{@p[:type]}" raise "unknown type for notification #{@item[:type]}"
end end
NotificationFactory::Mailer.notification( NotificationFactory::Mailer.notification(
@ -177,7 +182,7 @@ class Observer::Ticket::Notification::BackgroundJob
references: ticket.get_references, references: ticket.get_references,
main_object: ticket, main_object: ticket,
) )
Rails.logger.debug "sent ticket email notifiaction to agent (#{@p[:type]}/#{ticket.id}/#{user.email})" Rails.logger.debug "sent ticket email notifiaction to agent (#{@item[:type]}/#{ticket.id}/#{user.email})"
end end
end end
@ -200,14 +205,14 @@ class Observer::Ticket::Notification::BackgroundJob
def human_changes(user, record) def human_changes(user, record)
return {} if !@p[:changes] return {} if !@item[:changes]
locale = user.preferences[:locale] || 'en-us' locale = user.preferences[:locale] || 'en-us'
# only show allowed attributes # only show allowed attributes
attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user) attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user)
#puts "AL #{attribute_list.inspect}" #puts "AL #{attribute_list.inspect}"
user_related_changes = {} user_related_changes = {}
@p[:changes].each {|key, value| @item[:changes].each {|key, value|
# if no config exists, use all attributes # if no config exists, use all attributes
if !attribute_list || attribute_list.empty? if !attribute_list || attribute_list.empty?

View file

@ -0,0 +1,227 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
class Transaction::Slack
=begin
{
object: 'Ticket',
type: 'update',
ticket_id: 123,
via_web: true,
changes: {
'attribute1' => [before, now],
'attribute2' => [before, now],
}
},
=end
def initialize(item, params = {})
@item = item
@params = params
end
def perform
return if @item[:object] != 'Ticket'
return if !Setting.get('slack_integration')
logo_url = "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/assets/images/#{Setting.get('product_logo')}"
config = Setting.get('slack_config')
return if !config
return if !config['items']
ticket = Ticket.find(@item[:ticket_id])
if @item[:article_id]
article = Ticket::Article.find(@item[:article_id])
end
# ignore if no changes has been done
changes = human_changes(ticket)
return if @item[:type] == 'update' && !article && (!changes || changes.empty?)
# get user based notification template
# if create, send create message / block update messages
template = nil
if @item[:type] == 'create'
template = 'ticket_create'
elsif @item[:type] == 'update'
template = 'ticket_update'
elsif @item[:type] == 'reminder_reached'
template = 'ticket_reminder_reached'
elsif @item[:type] == 'escalation'
template = 'ticket_escalation'
elsif @item[:type] == 'escalation_warning'
template = 'ticket_escalation_warning'
else
raise "unknown type for notification #{@item[:type]}"
end
user = User.find(1)
result = NotificationFactory::Slack.template(
template: template,
locale: user[:preferences][:locale],
objects: {
ticket: ticket,
article: article,
changes: changes,
},
)
# good, warning, danger
color = '#000000'
ticket_state_type = ticket.state.state_type.name
if ticket.escalation_time && ticket.escalation_time > Time.zone.now
color = '#f35912'
elsif ticket_state_type == 'pending reminder'
if ticket.pending_time && ticket.pending_time < Time.zone.now
color = '#faab00'
end
elsif ticket_state_type =~ /^(new|open)$/
color = '#faab00'
elsif ticket_state_type == 'closed'
color = '#38ad69'
end
config['items'].each {|item|
# check action
if item['types']
hit = false
item['types'].each {|type|
next if type.to_s != @item[:type].to_s
hit = true
break
}
next if !hit
end
# check group
if item['group_ids']
hit = false
item['group_ids'].each {|group_id|
next if group_id.to_s != ticket.group_id.to_s
hit = true
break
}
next if !hit
end
Rails.logger.debug "sent webhook (#{@item[:type]}/#{ticket.id}/#{item['webhook']})"
notifier = Slack::Notifier.new(
item['webhook'],
channel: item['channel'],
username: item['username'],
icon_url: logo_url,
mrkdwn: true,
)
if item['expand']
body = "#{result[:subject]}\n#{result[:body]}"
result = notifier.ping body
else
attachment = {
text: result[:body],
mrkdwn_in: ['text'],
color: color,
}
result = notifier.ping result[:subject],
attachments: [attachment]
end
if !result
Rails.logger.error "Unable to post webhook: #{item['webhook']}"
end
if result.code.to_s != '200' && result.code.to_s != '201'
Rails.logger.error "Unable to post webhook: #{item['webhook']}: #{result.inspect}"
end
Rails.logger.debug "sent webhook (#{@item[:type]}/#{ticket.id}/#{item['webhook']})"
}
end
def human_changes(record)
return {} if !@item[:changes]
user = User.find(1)
locale = user.preferences[:locale] || 'en-us'
# only show allowed attributes
attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user)
#puts "AL #{attribute_list.inspect}"
user_related_changes = {}
@item[:changes].each {|key, value|
# if no config exists, use all attributes
if !attribute_list || attribute_list.empty?
user_related_changes[key] = value
# if config exists, just use existing attributes for user
elsif attribute_list[key.to_s]
user_related_changes[key] = value
end
}
changes = {}
user_related_changes.each {|key, value|
# get attribute name
attribute_name = key.to_s
object_manager_attribute = attribute_list[attribute_name]
if attribute_name[-3, 3] == '_id'
attribute_name = attribute_name[ 0, attribute_name.length - 3 ].to_s
end
# add item to changes hash
if key.to_s == attribute_name
changes[attribute_name] = value
end
# if changed item is an _id field/reference, do an lookup for the realy values
value_id = []
value_str = [ value[0], value[1] ]
if key.to_s[-3, 3] == '_id'
value_id[0] = value[0]
value_id[1] = value[1]
if record.respond_to?(attribute_name) && record.send(attribute_name)
relation_class = record.send(attribute_name).class
if relation_class && value_id[0]
relation_model = relation_class.lookup(id: value_id[0])
if relation_model
if relation_model['name']
value_str[0] = relation_model['name']
elsif relation_model.respond_to?('fullname')
value_str[0] = relation_model.send('fullname')
end
end
end
if relation_class && value_id[1]
relation_model = relation_class.lookup(id: value_id[1])
if relation_model
if relation_model['name']
value_str[1] = relation_model['name']
elsif relation_model.respond_to?('fullname')
value_str[1] = relation_model.send('fullname')
end
end
end
end
end
# check if we have an dedcated display name for it
display = attribute_name
if object_manager_attribute && object_manager_attribute[:display]
# delete old key
changes.delete(display)
# set new key
display = object_manager_attribute[:display].to_s
end
changes[display] = if object_manager_attribute && object_manager_attribute[:translate]
from = Translation.translate(locale, value_str[0])
to = Translation.translate(locale, value_str[1])
[from, to]
else
[value_str[0].to_s, value_str[1].to_s]
end
}
changes
end
end

View file

@ -14,7 +14,7 @@ Neues Ticket (<%= d 'ticket.title' %>)
<p> <p>
<%= t 'Information' %>: <%= t 'Information' %>:
<blockquote type="cite"> <blockquote type="cite">
<%= a 'article' %> <%= a_html 'article' %>
</blockquote> </blockquote>
</p> </p>
<% end %> <% end %>

View file

@ -14,7 +14,7 @@ New Ticket (<%= d 'ticket.title' %>)
<p> <p>
<%= t 'Information' %>: <%= t 'Information' %>:
<blockquote type="cite"> <blockquote type="cite">
<%= a 'article' %> <%= a_html 'article' %>
</blockquote> </blockquote>
</p> </p>
<% end %> <% end %>

View file

@ -8,7 +8,7 @@ Ticket ist eskaliert (<%= d 'ticket.title' %>)
<p> <p>
<%= t 'Information' %>: <%= t 'Information' %>:
<blockquote type="cite"> <blockquote type="cite">
<%= a 'article' %> <%= a_html 'article' %>
</blockquote> </blockquote>
</p> </p>
<% end %> <% end %>

View file

@ -8,7 +8,7 @@ Ticket is escalated (<%= d 'ticket.title' %>)
<p> <p>
<%= t 'Information' %>: <%= t 'Information' %>:
<blockquote type="cite"> <blockquote type="cite">
<%= a 'article' %> <%= a_html 'article' %>
</blockquote> </blockquote>
</p> </p>
<% end %> <% end %>

View file

@ -8,7 +8,7 @@ Ticket wird eskalieren (<%= d 'ticket.title' %>)
<p> <p>
<%= t 'Information' %>: <%= t 'Information' %>:
<blockquote type="cite"> <blockquote type="cite">
<%= a 'article' %> <%= a_html 'article' %>
</blockquote> </blockquote>
</p> </p>
<% end %> <% end %>

View file

@ -1,14 +1,14 @@
Ticket will escalated (<%= d 'ticket.title' %>) Ticket will escalate (<%= d 'ticket.title' %>)
<p>Hi <%= d 'recipient.firstname' %>,</p> <p>Hi <%= d 'recipient.firstname' %>,</p>
<br> <br>
<p>a ticket (<%= d 'ticket.title' %>) from "<b><%= d 'ticket.customer.longname' %></b>" will escalate at "<%= d 'ticket.escalation_time' %>"!</p> <p>A ticket (<%= d 'ticket.title' %>) from "<b><%= d 'ticket.customer.longname' %></b>" will escalate at "<%= d 'ticket.escalation_time' %>"!</p>
<br> <br>
<% if @objects[:article] %> <% if @objects[:article] %>
<p> <p>
<%= t 'Information' %>: <%= t 'Information' %>:
<blockquote type="cite"> <blockquote type="cite">
<%= a 'article' %> <%= a_html 'article' %>
</blockquote> </blockquote>
</p> </p>
<% end %> <% end %>

View file

@ -8,7 +8,7 @@ Warten auf Erinnerung erreicht! (<%= d 'ticket.title' %>)
<p> <p>
<%= t 'Information' %>: <%= t 'Information' %>:
<blockquote type="cite"> <blockquote type="cite">
<%= a 'article' %> <%= a_html 'article' %>
</blockquote> </blockquote>
</p> </p>
<% end %> <% end %>

View file

@ -2,13 +2,13 @@ Reminder reached (<%= d 'ticket.title' %>)
<p>Hi <%= d 'recipient.firstname' %>,</p> <p>Hi <%= d 'recipient.firstname' %>,</p>
<br> <br>
<p>Ticket needs attention, reminder reached for ticket (<%= d 'ticket.title' %>) with customer "<b><%= d 'ticket.customer.longname' %></b>".</p> <p>A ticket needs attention, reminder reached for (<%= d 'ticket.title' %>) with customer "<b><%= d 'ticket.customer.longname' %></b>".</p>
<br> <br>
<% if @objects[:article] %> <% if @objects[:article] %>
<p> <p>
<%= t 'Information' %>: <%= t 'Information' %>:
<blockquote type="cite"> <blockquote type="cite">
<%= a 'article' %> <%= a_html 'article' %>
</blockquote> </blockquote>
</p> </p>
<% end %> <% end %>

View file

@ -19,7 +19,7 @@ Ticket (<%= d 'ticket.title' %>) wurde von "<b><%= d 'ticket.updated_by.longname
<p> <p>
<%= t 'Information' %>: <%= t 'Information' %>:
<blockquote type="cite"> <blockquote type="cite">
<%= a 'article' %> <%= a_html 'article' %>
</blockquote> </blockquote>
</p> </p>
<% end %> <% end %>

View file

@ -19,7 +19,7 @@ Ticket (<%= d 'ticket.title' %>) has been updated by "<b><%= d 'ticket.updated_b
<p> <p>
<%= t 'Information' %>: <%= t 'Information' %>:
<blockquote type="cite"> <blockquote type="cite">
<%= a 'article' %> <%= a_html 'article' %>
</blockquote> </blockquote>
</p> </p>
<% end %> <% end %>

View file

@ -0,0 +1 @@
<%= d 'message', false %>

View file

@ -0,0 +1,9 @@
# <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Created by <%= d 'ticket.updated_by.longname' %> at <%= d 'ticket.updated_at' %>_
* <%= t 'Group' %>: <%= d 'ticket.group.name' %>
* <%= t 'Owner' %>: <%= d 'ticket.owner.fullname' %>
* <%= t 'State' %>: <%= t d 'ticket.state.name' %>
<% if @objects[:article] %>
<%= a_text 'article' %>
<% end %>

View file

@ -0,0 +1,7 @@
# <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Escalated at <%= d 'ticket.escalation_time' %>_
A ticket (<%= d 'ticket.title' %>) from "<b><%= d 'ticket.customer.longname' %></b>" is escalated since "<%= d 'ticket.escalation_time' %>"!
<% if @objects[:article] %>
<%= a_text 'article' %>
<% end %>

View file

@ -0,0 +1,7 @@
# <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Will escalate at <%= d 'ticket.escalation_time' %>_
A ticket (<%= d 'ticket.title' %>) from "<b><%= d 'ticket.customer.longname' %></b>" will escalate at "<%= d 'ticket.escalation_time' %>"!
<% if @objects[:article] %>
<%= a_text 'article' %>
<% end %>

View file

@ -0,0 +1,7 @@
# <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Reminder reached!_
A ticket needs attention, reminder reached for (<%= d 'ticket.title' %>) with customer "*<%= d 'ticket.customer.longname' %>*".
<% if @objects[:article] %>
<%= a_text 'article' %>
<% end %>

View file

@ -0,0 +1,11 @@
# <%= d 'ticket.title' %>
_<<%= c 'http_type' %>://<%= c 'fqdn' %>/#ticket/zoom/<%= d 'ticket.id' %>|Ticket#<%= d 'ticket.number' %>>: Updated by <%= d 'ticket.updated_by.longname' %> at <%= d 'ticket.updated_at' %>_
<% if @objects[:changes] && !@objects[:changes].empty? %>
<% @objects[:changes].each do |key, value| %>
* <%= t key %>: <%= h value[0] %> -> <%= h value[1] %>
<% end %>
<% end %>
<% if @objects[:article] %>
<%= a_text 'article' %>
<% end %>

View file

@ -31,7 +31,6 @@ module Zammad
'observer::_ticket::_article::_communicate_facebook', 'observer::_ticket::_article::_communicate_facebook',
'observer::_ticket::_article::_communicate_twitter', 'observer::_ticket::_article::_communicate_twitter',
'observer::_ticket::_article::_signature_detection', 'observer::_ticket::_article::_signature_detection',
'observer::_ticket::_notification',
'observer::_ticket::_reset_new_state', 'observer::_ticket::_reset_new_state',
'observer::_ticket::_escalation_calculation', 'observer::_ticket::_escalation_calculation',
'observer::_ticket::_ref_object_touch', 'observer::_ticket::_ref_object_touch',
@ -42,7 +41,8 @@ module Zammad
'observer::_user::_ticket_organization', 'observer::_user::_ticket_organization',
'observer::_user::_geo', 'observer::_user::_geo',
'observer::_organization::_ref_object_touch', 'observer::_organization::_ref_object_touch',
'observer::_sla::_ticket_rebuild_escalation' 'observer::_sla::_ticket_rebuild_escalation',
'observer::_transaction'
# REST api path # REST api path
config.api_path = '/api/v1' config.api_path = '/api/v1'

View file

@ -0,0 +1,144 @@
class AddSlackIntegration < ActiveRecord::Migration
def up
Setting.create_or_update(
title: 'Icinga integration',
name: 'icinga_integration',
area: 'Integration::Switch',
description: 'Define if Icinga (http://www.icinga.org) is enabled or not.',
options: {
form: [
{
display: '',
null: true,
name: 'icinga_integration',
tag: 'boolean',
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: false,
preferences: { prio: 1 },
frontend: false
)
Setting.create_or_update(
title: 'Sender',
name: 'icinga_sender',
area: 'Integration::Icinga',
description: 'Define the sender email address of Icinga emails.',
options: {
form: [
{
display: '',
null: false,
name: 'icinga_sender',
tag: 'input',
placeholder: 'icinga@monitoring.example.com',
},
],
},
state: 'icinga@monitoring.example.com',
frontend: false,
preferences: { prio: 2 },
)
Setting.create_or_update(
title: 'Nagios integration',
name: 'nagios_integration',
area: 'Integration::Switch',
description: 'Define if Nagios (http://www.nagios.org) is enabled or not.',
options: {
form: [
{
display: '',
null: true,
name: 'nagios_integration',
tag: 'boolean',
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: false,
preferences: { prio: 1 },
frontend: false
)
Setting.create_or_update(
title: 'Sender',
name: 'nagios_sender',
area: 'Integration::Nagios',
description: 'Define the sender email address of Nagios emails.',
options: {
form: [
{
display: '',
null: false,
name: 'nagios_sender',
tag: 'input',
placeholder: 'nagios@monitoring.example.com',
},
],
},
state: 'nagios@monitoring.example.com',
frontend: false,
preferences: { prio: 2 },
)
Setting.create_or_update(
title: 'Define transaction backend.',
name: '0100_notification',
area: 'Transaction::Backend',
description: 'Define the transaction backend to send agent notifications.',
options: {},
state: 'Transaction::Notification',
frontend: false
)
Setting.create_or_update(
title: 'Define transaction backend.',
name: '6000_slack_webhook',
area: 'Transaction::Backend',
description: 'Define the transaction backend which posts messages to (http://www.slack.com).',
options: {},
state: 'Transaction::Slack',
frontend: false
)
Setting.create_if_not_exists(
title: 'Slack integration',
name: 'slack_integration',
area: 'Integration::Slack',
description: 'Define if Slack (http://www.slack.org) is enabled or not.',
options: {
form: [
{
display: '',
null: true,
name: 'slack_integration',
tag: 'boolean',
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: true,
preferences: { prio: 1 },
frontend: false
)
Setting.create_or_update(
title: 'Slack config',
name: 'slack_config',
area: 'Integration::Slack',
description: 'Define the slack config.',
options: {},
state: {
items: []
},
frontend: false,
preferences: { prio: 2 },
)
end
end

View file

@ -1600,7 +1600,7 @@ Setting.create_if_not_exists(
Setting.create_if_not_exists( Setting.create_if_not_exists(
title: 'Icinga integration', title: 'Icinga integration',
name: 'icinga_integration', name: 'icinga_integration',
area: 'Integration::Icinga', area: 'Integration::Switch',
description: 'Define if Icinga (http://www.icinga.org) is enabled or not.', description: 'Define if Icinga (http://www.icinga.org) is enabled or not.',
options: { options: {
form: [ form: [
@ -1632,6 +1632,7 @@ Setting.create_if_not_exists(
null: false, null: false,
name: 'icinga_sender', name: 'icinga_sender',
tag: 'input', tag: 'input',
placeholder: 'icinga@monitoring.example.com',
}, },
], ],
}, },
@ -1685,7 +1686,7 @@ Setting.create_if_not_exists(
Setting.create_if_not_exists( Setting.create_if_not_exists(
title: 'Nagios integration', title: 'Nagios integration',
name: 'nagios_integration', name: 'nagios_integration',
area: 'Integration::Nagios', area: 'Integration::Switch',
description: 'Define if Nagios (http://www.nagios.org) is enabled or not.', description: 'Define if Nagios (http://www.nagios.org) is enabled or not.',
options: { options: {
form: [ form: [
@ -1717,6 +1718,7 @@ Setting.create_if_not_exists(
null: false, null: false,
name: 'nagios_sender', name: 'nagios_sender',
tag: 'input', tag: 'input',
placeholder: 'nagios@monitoring.example.com',
}, },
], ],
}, },
@ -1767,6 +1769,59 @@ Setting.create_if_not_exists(
preferences: { prio: 4 }, preferences: { prio: 4 },
frontend: false frontend: false
) )
Setting.create_if_not_exists(
title: 'Define transaction backend.',
name: '0100_notification',
area: 'Transaction::Backend',
description: 'Define the transaction backend to send agent notifications.',
options: {},
state: 'Transaction::Notification',
frontend: false
)
Setting.create_if_not_exists(
title: 'Define transaction backend.',
name: '6000_slack_webhook',
area: 'Transaction::Backend',
description: 'Define the transaction backend which posts messages to (http://www.slack.com).',
options: {},
state: 'Transaction::Slack',
frontend: false
)
Setting.create_if_not_exists(
title: 'Slack integration',
name: 'slack_integration',
area: 'Integration::Switch',
description: 'Define if Slack (http://www.slack.org) is enabled or not.',
options: {
form: [
{
display: '',
null: true,
name: 'slack_integration',
tag: 'boolean',
options: {
true => 'yes',
false => 'no',
},
},
],
},
state: true,
preferences: { prio: 1 },
frontend: false
)
Setting.create_if_not_exists(
title: 'Slack config',
name: 'slack_config',
area: 'Integration::Slack',
description: 'Define the slack config.',
options: {},
state: {
items: []
},
frontend: false,
preferences: { prio: 2 },
)
signature = Signature.create_if_not_exists( signature = Signature.create_if_not_exists(
id: 1, id: 1,

View file

@ -5,8 +5,17 @@ module NotificationFactory
result = NotificationFactory.template_read( result = NotificationFactory.template_read(
template: 'password_reset', template: 'password_reset',
locale: 'en-us', locale: 'en-us',
format: 'html', # md format: 'html',
type: 'mailer', # slack type: 'mailer',
)
or
result = NotificationFactory.template_read(
template: 'ticket_update',
locale: 'en-us',
format: 'md',
type: 'slack',
) )
returns returns
@ -56,8 +65,15 @@ returns
=begin =begin
string = NotificationFactory.application_template_read( string = NotificationFactory.application_template_read(
format: 'html', # md format: 'html',
type: 'mailer', # slack type: 'mailer',
)
or
string = NotificationFactory.application_template_read(
format: 'md',
type: 'slack',
) )
returns returns

View file

@ -0,0 +1,54 @@
class NotificationFactory::Slack
=begin
result = NotificationFactory::Slack.template(
template: 'ticket_update',
locale: 'en-us',
objects: {
recipient: User.find(2),
ticket: Ticket.find(1)
},
)
returns
{
subject: 'some subject',
body: 'some body',
}
=end
def self.template(data)
if data[:templateInline]
return NotificationFactory::Template.new(data[:objects], data[:locale], data[:templateInline]).render
end
template = NotificationFactory.template_read(
locale: data[:locale] || 'en',
template: data[:template],
format: 'md',
type: 'slack',
)
message_subject = NotificationFactory::Template.new(data[:objects], data[:locale], template[:subject]).render
message_body = NotificationFactory::Template.new(data[:objects], data[:locale], template[:body]).render
if !data[:raw]
application_template = NotificationFactory.application_template_read(
format: 'md',
type: 'slack',
)
data[:objects][:message] = message_body
data[:objects][:standalone] = data[:standalone]
message_body = NotificationFactory::Template.new(data[:objects], data[:locale], application_template).render
end
{
subject: message_subject.strip!,
body: message_body.strip!,
}
end
end

View file

@ -11,6 +11,8 @@ class NotificationFactory::Template
ERB.new(@template).result(binding) ERB.new(@template).result(binding)
end end
# d - data of object
# d('user.firstname', htmlEscape)
def d(key, escape = nil) def d(key, escape = nil)
# do validaton, ignore some methodes # do validaton, ignore some methodes
@ -45,19 +47,25 @@ class NotificationFactory::Template
h placeholder h placeholder
end end
# c - config
# c('fqdn', htmlEscape)
def c(key, escape = nil) def c(key, escape = nil)
config = Setting.get(key) config = Setting.get(key)
return config if escape == false || (escape.nil? && !@escape) return config if escape == false || (escape.nil? && !@escape)
h config h config
end end
# t - translation
# t('yes', htmlEscape)
def t(key, escape = nil) def t(key, escape = nil)
translation = Translation.translate(@locale, key) translation = Translation.translate(@locale, key)
return translation if escape == false || (escape.nil? && !@escape) return translation if escape == false || (escape.nil? && !@escape)
h translation h translation
end end
def a(article) # a_html - article body in html
# a_html(article)
def a_html(article)
content_type = d "#{article}.content_type", false content_type = d "#{article}.content_type", false
if content_type =~ /html/ if content_type =~ /html/
return d "#{article}.body", false return d "#{article}.body", false
@ -65,6 +73,19 @@ class NotificationFactory::Template
d("#{article}.body", false).text2html d("#{article}.body", false).text2html
end end
# a_text - article body in text
# a_text(article)
def a_text(article)
content_type = d "#{article}.content_type", false
body = d "#{article}.body", false
if content_type =~ /html/
body = body.html2text
end
(body.strip + "\n").gsub(/^(.*?)$/, '> \\1')
end
# h - htmlEscape
# h('fqdn', htmlEscape)
def h(key) def h(key)
return key if !key return key if !key
CGI.escapeHTML(key.to_s) CGI.escapeHTML(key.to_s)

View file

@ -0,0 +1,192 @@
# encoding: utf-8
require 'integration_test_helper'
require 'slack'
class SlackTest < ActiveSupport::TestCase
# needed to check correct behavior
slack_group = Group.create_if_not_exists(
name: 'Slack',
updated_by_id: 1,
created_by_id: 1
)
# check
test 'base' do
if !ENV['SLACK_CI_CHANNEL']
raise "ERROR: Need SLACK_CI_CHANNEL - hint SLACK_CI_CHANNEL='ci-zammad'"
end
if !ENV['SLACK_CI_WEBHOOK']
raise "ERROR: Need SLACK_CI_WEBHOOK - hint SLACK_CI_WEBHOOK='https://hooks.slack.com/services/...'"
end
if !ENV['SLACK_CI_CHECKER_TOKEN']
raise "ERROR: Need SLACK_CI_CHECKER_TOKEN - hint SLACK_CI_CHECKER_TOKEN='...'"
end
channel = ENV['SLACK_CI_CHANNEL']
webhook = ENV['SLACK_CI_WEBHOOK']
# set system mode to done / to activate
Setting.set('system_init_done', true)
Setting.set('slack_integration', true)
items = [
{
group_ids: [slack_group.id],
types: %w(create update),
webhook: webhook,
channel: channel,
username: 'zammad bot',
expand: false,
}
]
Setting.set('slack_config', { items: items })
# case 1
customer = User.find(2)
hash = hash_gen
text = "#{rand_word}... #{hash}"
default_group = Group.first
ticket1 = Ticket.create(
title: text,
customer_id: customer.id,
group_id: default_group.id,
state: Ticket::State.find_by(name: 'new'),
priority: Ticket::Priority.find_by(name: '2 normal'),
updated_by_id: 1,
created_by_id: 1,
)
article1 = Ticket::Article.create(
ticket_id: ticket1.id,
body: text,
type: Ticket::Article::Type.find_by(name: 'note'),
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
internal: false,
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
Delayed::Worker.new.work_off
# check if message exists
assert_not(slack_check(channel, hash))
ticket1.state = Ticket::State.find_by(name: 'open')
ticket1.save
Observer::Transaction.commit
Delayed::Worker.new.work_off
# check if message exists
assert_not(slack_check(channel, hash))
# case 2
hash = hash_gen
text = "#{rand_word}... #{hash}"
ticket2 = Ticket.create(
title: text,
customer_id: customer.id,
group_id: slack_group.id,
state: Ticket::State.find_by(name: 'new'),
priority: Ticket::Priority.find_by(name: '2 normal'),
updated_by_id: 1,
created_by_id: 1,
)
article2 = Ticket::Article.create(
ticket_id: ticket2.id,
body: text,
type: Ticket::Article::Type.find_by(name: 'note'),
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
internal: false,
updated_by_id: 1,
created_by_id: 1,
)
Observer::Transaction.commit
Delayed::Worker.new.work_off
# check if message exists
assert(slack_check(channel, hash))
hash = hash_gen
text = "#{rand_word}... #{hash}"
ticket2.title = text
ticket2.save
Observer::Transaction.commit
Delayed::Worker.new.work_off
# check if message exists
assert(slack_check(channel, hash))
end
def hash_gen
(0...10).map { ('a'..'z').to_a[rand(26)] }.join
end
def rand_word
words = [
'dog',
'cat',
'house',
'home',
'yesterday',
'tomorrow',
'new york',
'berlin',
'coffee script',
'java script',
'bob smith',
'be open',
'really nice',
'stay tuned',
'be a good boy',
'invent new things',
]
words[rand(words.length)]
end
def slack_check(channel_name, search_for)
Slack.configure do |config|
config.token = ENV['SLACK_CI_CHECKER_TOKEN']
end
Slack.auth_test
client = Slack::Client.new
channels = client.channels_list['channels']
channel_id = nil
channels.each {|channel|
next if channel['name'] != channel_name
channel_id = channel['id']
}
if !channel_id
raise "ERROR: No such channel '#{channel_name}'"
end
channel_history = client.channels_history(channel: channel_id)
if !channel_history
raise "ERROR: No history for channel #{channel_name}/#{channel_id}"
end
if !channel_history['messages']
raise "ERROR: No history messages for channel #{channel_name}/#{channel_id}"
end
channel_history['messages'].each {|message|
next if !message['text']
if message['text'] =~ /#{search_for}/i
p "SUCCESS: message with #{search_for} found!"
return true
end
}
#raise "ERROR: No such message containing #{search_for} in history of channel #{channel_name}/#{channel_id}"
false
end
end

View file

@ -864,8 +864,8 @@ class TicketNotificationTest < ActiveSupport::TestCase
ticket1.priority = Ticket::Priority.lookup(name: '3 high') ticket1.priority = Ticket::Priority.lookup(name: '3 high')
ticket1.save ticket1.save
list = EventBuffer.list('notification') list = EventBuffer.list('transaction')
list_objects = Observer::Ticket::Notification.get_uniq_changes(list) list_objects = Observer::Transaction.get_uniq_changes(list)
assert_equal('some notification event test 1', list_objects[ticket1.id][:changes]['title'][0]) assert_equal('some notification event test 1', list_objects[ticket1.id][:changes]['title'][0])
assert_equal('some notification event test 1 - #2', list_objects[ticket1.id][:changes]['title'][1]) assert_equal('some notification event test 1 - #2', list_objects[ticket1.id][:changes]['title'][1])
@ -878,8 +878,8 @@ class TicketNotificationTest < ActiveSupport::TestCase
ticket1.priority = Ticket::Priority.lookup(name: '1 low') ticket1.priority = Ticket::Priority.lookup(name: '1 low')
ticket1.save ticket1.save
list = EventBuffer.list('notification') list = EventBuffer.list('transaction')
list_objects = Observer::Ticket::Notification.get_uniq_changes(list) list_objects = Observer::Transaction.get_uniq_changes(list)
assert_equal('some notification event test 1', list_objects[ticket1.id][:changes]['title'][0]) assert_equal('some notification event test 1', list_objects[ticket1.id][:changes]['title'][0])
assert_equal('some notification event test 1 - #2 - #3', list_objects[ticket1.id][:changes]['title'][1]) assert_equal('some notification event test 1 - #2 - #3', list_objects[ticket1.id][:changes]['title'][1])
@ -916,7 +916,7 @@ class TicketNotificationTest < ActiveSupport::TestCase
) )
assert(ticket1, 'ticket created - ticket notification template') assert(ticket1, 'ticket created - ticket notification template')
bg = Observer::Ticket::Notification::BackgroundJob.new( bg = Transaction::Notification.new(
ticket_id: ticket1.id, ticket_id: ticket1.id,
article_id: article.id, article_id: article.id,
type: 'update', type: 'update',
@ -992,7 +992,7 @@ class TicketNotificationTest < ActiveSupport::TestCase
assert_no_match(/pending_till/, result[:body]) assert_no_match(/pending_till/, result[:body])
assert_no_match(/i18n/, result[:body]) assert_no_match(/i18n/, result[:body])
bg = Observer::Ticket::Notification::BackgroundJob.new( bg = Transaction::Notification.new(
ticket_id: ticket1.id, ticket_id: ticket1.id,
article_id: article.id, article_id: article.id,
type: 'update', type: 'update',