<%- @T(item[0], item[1], item[2]) %>
+ <% end %> + <% end %> + +diff --git a/Gemfile b/Gemfile index ecc9a8afd..a188559e6 100644 --- a/Gemfile +++ b/Gemfile @@ -88,6 +88,9 @@ gem 'writeexcel' gem 'icalendar' gem 'browser' +# integrations +gem 'slack-notifier' + # event machine gem 'eventmachine' gem 'em-websocket' diff --git a/Gemfile.lock b/Gemfile.lock index c0deb2051..f568c61ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -267,6 +267,7 @@ GEM simplecov-html (0.10.0) simplecov-rcov (0.2.3) simplecov (>= 0.4.1) + slack-notifier (1.5.1) slop (3.6.0) spring (1.6.4) sprockets (3.5.2) @@ -361,6 +362,7 @@ DEPENDENCIES simple-rss simplecov simplecov-rcov + slack-notifier spring sprockets sqlite3 diff --git a/app/assets/javascripts/app/controllers/_integration/icinga.coffee b/app/assets/javascripts/app/controllers/_integration/icinga.coffee index a5ad04487..3c486a06b 100644 --- a/app/assets/javascripts/app/controllers/_integration/icinga.coffee +++ b/app/assets/javascripts/app/controllers/_integration/icinga.coffee @@ -1,12 +1,30 @@ -class Icinga extends App.ControllerTabs - header: 'Icinga' - constructor: -> - super - return if !@authenticate(false, 'Admin') - @title 'Icinga', true - @tabs = [ - { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Integration::Icinga' } } - ] - @render() +class Index extends App.ControllerIntegrationBase + featureIntegration: 'icinga_integration' + featureName: 'Icinga' + featureConfig: 'icinga_config' + description: [ + ['This service receives emails from %s and creates tickets with host and service.', 'Icinga'] + ['If the host and service is recovered again, the ticket will be closed automatically.'] + ] -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' +) diff --git a/app/assets/javascripts/app/controllers/_integration/mattermost.coffee b/app/assets/javascripts/app/controllers/_integration/mattermost.coffee deleted file mode 100644 index ce340da0d..000000000 --- a/app/assets/javascripts/app/controllers/_integration/mattermost.coffee +++ /dev/null @@ -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') diff --git a/app/assets/javascripts/app/controllers/_integration/nagios.coffee b/app/assets/javascripts/app/controllers/_integration/nagios.coffee index 471fa8516..ab8bf6394 100644 --- a/app/assets/javascripts/app/controllers/_integration/nagios.coffee +++ b/app/assets/javascripts/app/controllers/_integration/nagios.coffee @@ -1,12 +1,30 @@ -class Nagios extends App.ControllerTabs - header: 'Nagios' - constructor: -> - super - return if !@authenticate(false, 'Admin') - @title 'Nagios', true - @tabs = [ - { name: 'Base', 'target': 'base', controller: App.SettingsArea, params: { area: 'Integration::Nagios' } } - ] - @render() +class Index extends App.ControllerIntegrationBase + featureIntegration: 'nagios_integration' + featureName: 'Nagios' + featureConfig: 'nagios_config' + description: [ + ['This service receives emails from %s and creates tickets with host and service.', 'Nagios'] + ['If the host and service is recovered again, the ticket will be closed automatically.'] + ] -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' +) diff --git a/app/assets/javascripts/app/controllers/_integration/slack.coffee b/app/assets/javascripts/app/controllers/_integration/slack.coffee new file mode 100644 index 000000000..de3169477 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_integration/slack.coffee @@ -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' +) diff --git a/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee b/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee index 893498d51..587b2132e 100644 --- a/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee +++ b/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee @@ -37,7 +37,7 @@ class Index extends App.Controller } auth_providers = [] 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 @html App.view('profile/linked_accounts')( @@ -52,19 +52,19 @@ class Index extends App.Controller # get data @ajax( - id: 'account' - type: 'DELETE' - url: @apiPath + '/users/account' - data: JSON.stringify({ provider: provider, uid: uid }) + id: 'account' + type: 'DELETE' + url: @apiPath + '/users/account' + data: JSON.stringify(provider: provider, uid: uid) processData: true - success: @success - error: @error + success: @success + error: @error ) success: (data, status, xhr) => @notify( type: 'success' - msg: App.i18n.translateContent( 'Successfully!' ) + msg: App.i18n.translateContent('Successfully!') ) update = => @render() @@ -72,10 +72,10 @@ class Index extends App.Controller error: (xhr, status, error) => @render() - data = JSON.parse( xhr.responseText ) + data = JSON.parse(xhr.responseText) @notify( 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') diff --git a/app/assets/javascripts/app/controllers/_settings/area.coffee b/app/assets/javascripts/app/controllers/_settings/area.coffee index 36a718730..f6bdc1674 100644 --- a/app/assets/javascripts/app/controllers/_settings/area.coffee +++ b/app/assets/javascripts/app/controllers/_settings/area.coffee @@ -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 constructor: -> super @@ -115,11 +205,11 @@ class App.SettingsAreaItem extends App.Controller if @setting.preferences.session_check App.Auth.loginCheck() - fail: -> + fail: (settings, details) -> ui.formEnable(e) App.Event.trigger 'notify', { 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 } ) diff --git a/app/assets/javascripts/app/controllers/integrations.coffee b/app/assets/javascripts/app/controllers/integrations.coffee index 5de52cf83..57570bf1c 100644 --- a/app/assets/javascripts/app/controllers/integrations.coffee +++ b/app/assets/javascripts/app/controllers/integrations.coffee @@ -1,8 +1,41 @@ -class IndexRouter extends App.ControllerNavSidbar - authenticateRequired: true - configKey: 'NavBarIntegration' +class Index extends App.ControllerContent + constructor: -> + super -App.Config.set('integration', IndexRouter, 'Routes') -App.Config.set('integration/:target', IndexRouter, 'Routes') + # check authentication + 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') diff --git a/app/assets/javascripts/app/controllers/manage.coffee b/app/assets/javascripts/app/controllers/manage.coffee index f59d7bb72..5ab127c7d 100644 --- a/app/assets/javascripts/app/controllers/manage.coffee +++ b/app/assets/javascripts/app/controllers/manage.coffee @@ -8,6 +8,7 @@ App.Config.set('settings/:target', IndexRouter, 'Routes') App.Config.set('channels/:target', IndexRouter, 'Routes') App.Config.set('channels/:target/:channel_id', 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('Channels', { prio: 2500, name: 'Channels', target: '#channels', role: ['Admin'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/package.coffee b/app/assets/javascripts/app/controllers/package.coffee index e2c53ae30..a110789cd 100644 --- a/app/assets/javascripts/app/controllers/package.coffee +++ b/app/assets/javascripts/app/controllers/package.coffee @@ -58,4 +58,4 @@ class Index extends App.ControllerContent @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') diff --git a/app/assets/javascripts/app/models/setting.coffee b/app/assets/javascripts/app/models/setting.coffee index 1879df407..baedf47f0 100644 --- a/app/assets/javascripts/app/models/setting.coffee +++ b/app/assets/javascripts/app/models/setting.coffee @@ -2,3 +2,13 @@ class App.Setting extends App.Model @configure 'Setting', 'name', 'state_current' @extend Spine.Model.Ajax @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) diff --git a/app/assets/javascripts/app/views/integration/base.jst.eco b/app/assets/javascripts/app/views/integration/base.jst.eco new file mode 100644 index 000000000..d941bd965 --- /dev/null +++ b/app/assets/javascripts/app/views/integration/base.jst.eco @@ -0,0 +1,17 @@ +
<%- @T(item[0], item[1], item[2]) %>
+ <% end %> + <% end %> + ++ | <%- @T('Service') %> | +<%- @T('Description') %> | +
---|---|---|
+ <% if !integration.state.current(): %> + <%- @Icon('status', 'inactive inline') %> + <% else: %> + <%- @Icon('status', 'ok inline') %> + <% end %> + | +<%= integration.name %> | +<%= integration.description %> | +
<%= t 'Information' %>:
- <%= a 'article' %> + <%= a_html 'article' %><% end %> diff --git a/app/views/mailer/ticket_create/en.html.erb b/app/views/mailer/ticket_create/en.html.erb index 06cb4bb28..7a71d711d 100644 --- a/app/views/mailer/ticket_create/en.html.erb +++ b/app/views/mailer/ticket_create/en.html.erb @@ -14,7 +14,7 @@ New Ticket (<%= d 'ticket.title' %>)
<%= t 'Information' %>:
- <%= a 'article' %> + <%= a_html 'article' %><% end %> diff --git a/app/views/mailer/ticket_escalation/de.html.erb b/app/views/mailer/ticket_escalation/de.html.erb index 1b33dcddf..c32ac9300 100644 --- a/app/views/mailer/ticket_escalation/de.html.erb +++ b/app/views/mailer/ticket_escalation/de.html.erb @@ -8,7 +8,7 @@ Ticket ist eskaliert (<%= d 'ticket.title' %>)
<%= t 'Information' %>:
- <%= a 'article' %> + <%= a_html 'article' %><% end %> diff --git a/app/views/mailer/ticket_escalation/en.html.erb b/app/views/mailer/ticket_escalation/en.html.erb index 017472a7e..4bda1e9c2 100644 --- a/app/views/mailer/ticket_escalation/en.html.erb +++ b/app/views/mailer/ticket_escalation/en.html.erb @@ -8,7 +8,7 @@ Ticket is escalated (<%= d 'ticket.title' %>)
<%= t 'Information' %>:
- <%= a 'article' %> + <%= a_html 'article' %><% end %> diff --git a/app/views/mailer/ticket_escalation_warning/de.html.erb b/app/views/mailer/ticket_escalation_warning/de.html.erb index 4ee2e9671..b257a2142 100644 --- a/app/views/mailer/ticket_escalation_warning/de.html.erb +++ b/app/views/mailer/ticket_escalation_warning/de.html.erb @@ -8,7 +8,7 @@ Ticket wird eskalieren (<%= d 'ticket.title' %>)
<%= t 'Information' %>:
- <%= a 'article' %> + <%= a_html 'article' %><% end %> diff --git a/app/views/mailer/ticket_escalation_warning/en.html.erb b/app/views/mailer/ticket_escalation_warning/en.html.erb index d376e0c20..341663a1b 100644 --- a/app/views/mailer/ticket_escalation_warning/en.html.erb +++ b/app/views/mailer/ticket_escalation_warning/en.html.erb @@ -1,14 +1,14 @@ -Ticket will escalated (<%= d 'ticket.title' %>) +Ticket will escalate (<%= d 'ticket.title' %>)
Hi <%= d 'recipient.firstname' %>,
a ticket (<%= d 'ticket.title' %>) from "<%= d 'ticket.customer.longname' %>" will escalate at "<%= d 'ticket.escalation_time' %>"!
+A ticket (<%= d 'ticket.title' %>) from "<%= d 'ticket.customer.longname' %>" will escalate at "<%= d 'ticket.escalation_time' %>"!
<%= t 'Information' %>:
- <%= a 'article' %> + <%= a_html 'article' %><% end %> diff --git a/app/views/mailer/ticket_reminder_reached/de.html.erb b/app/views/mailer/ticket_reminder_reached/de.html.erb index 5e0c98911..e919e7dfa 100644 --- a/app/views/mailer/ticket_reminder_reached/de.html.erb +++ b/app/views/mailer/ticket_reminder_reached/de.html.erb @@ -8,7 +8,7 @@ Warten auf Erinnerung erreicht! (<%= d 'ticket.title' %>)
<%= t 'Information' %>:
- <%= a 'article' %> + <%= a_html 'article' %><% end %> diff --git a/app/views/mailer/ticket_reminder_reached/en.html.erb b/app/views/mailer/ticket_reminder_reached/en.html.erb index 91830d753..255614252 100644 --- a/app/views/mailer/ticket_reminder_reached/en.html.erb +++ b/app/views/mailer/ticket_reminder_reached/en.html.erb @@ -2,13 +2,13 @@ Reminder reached (<%= d 'ticket.title' %>)
Hi <%= d 'recipient.firstname' %>,
Ticket needs attention, reminder reached for ticket (<%= d 'ticket.title' %>) with customer "<%= d 'ticket.customer.longname' %>".
+A ticket needs attention, reminder reached for (<%= d 'ticket.title' %>) with customer "<%= d 'ticket.customer.longname' %>".
<%= t 'Information' %>:
- <%= a 'article' %> + <%= a_html 'article' %><% end %> diff --git a/app/views/mailer/ticket_update/de.html.erb b/app/views/mailer/ticket_update/de.html.erb index 8f47080e3..576be3f21 100644 --- a/app/views/mailer/ticket_update/de.html.erb +++ b/app/views/mailer/ticket_update/de.html.erb @@ -19,7 +19,7 @@ Ticket (<%= d 'ticket.title' %>) wurde von "<%= d 'ticket.updated_by.longname
<%= t 'Information' %>:
- <%= a 'article' %> + <%= a_html 'article' %><% end %> diff --git a/app/views/mailer/ticket_update/en.html.erb b/app/views/mailer/ticket_update/en.html.erb index 000701a03..df29d9ac0 100644 --- a/app/views/mailer/ticket_update/en.html.erb +++ b/app/views/mailer/ticket_update/en.html.erb @@ -19,7 +19,7 @@ Ticket (<%= d 'ticket.title' %>) has been updated by "<%= d 'ticket.updated_b
<%= t 'Information' %>:
- <%= a 'article' %> + <%= a_html 'article' %><% end %> diff --git a/app/views/slack/application.md.erb b/app/views/slack/application.md.erb new file mode 100644 index 000000000..3fec9ad1d --- /dev/null +++ b/app/views/slack/application.md.erb @@ -0,0 +1 @@ +<%= d 'message', false %> \ No newline at end of file diff --git a/app/views/slack/ticket_create/en.md.erb b/app/views/slack/ticket_create/en.md.erb new file mode 100644 index 000000000..594fac928 --- /dev/null +++ b/app/views/slack/ticket_create/en.md.erb @@ -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 %> diff --git a/app/views/slack/ticket_escalation/en.md.erb b/app/views/slack/ticket_escalation/en.md.erb new file mode 100644 index 000000000..330998489 --- /dev/null +++ b/app/views/slack/ticket_escalation/en.md.erb @@ -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 "<%= d 'ticket.customer.longname' %>" is escalated since "<%= d 'ticket.escalation_time' %>"! + +<% if @objects[:article] %> +<%= a_text 'article' %> +<% end %> diff --git a/app/views/slack/ticket_escalation_warning/en.md.erb b/app/views/slack/ticket_escalation_warning/en.md.erb new file mode 100644 index 000000000..15f48c0a3 --- /dev/null +++ b/app/views/slack/ticket_escalation_warning/en.md.erb @@ -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 "<%= d 'ticket.customer.longname' %>" will escalate at "<%= d 'ticket.escalation_time' %>"! + +<% if @objects[:article] %> +<%= a_text 'article' %> +<% end %> diff --git a/app/views/slack/ticket_reminder_reached/en.md.erb b/app/views/slack/ticket_reminder_reached/en.md.erb new file mode 100644 index 000000000..8ae47fc71 --- /dev/null +++ b/app/views/slack/ticket_reminder_reached/en.md.erb @@ -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 %> diff --git a/app/views/slack/ticket_update/en.md.erb b/app/views/slack/ticket_update/en.md.erb new file mode 100644 index 000000000..e391ad115 --- /dev/null +++ b/app/views/slack/ticket_update/en.md.erb @@ -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 %> diff --git a/config/application.rb b/config/application.rb index 59a17ebcf..4228be027 100644 --- a/config/application.rb +++ b/config/application.rb @@ -31,7 +31,6 @@ module Zammad 'observer::_ticket::_article::_communicate_facebook', 'observer::_ticket::_article::_communicate_twitter', 'observer::_ticket::_article::_signature_detection', - 'observer::_ticket::_notification', 'observer::_ticket::_reset_new_state', 'observer::_ticket::_escalation_calculation', 'observer::_ticket::_ref_object_touch', @@ -42,7 +41,8 @@ module Zammad 'observer::_user::_ticket_organization', 'observer::_user::_geo', 'observer::_organization::_ref_object_touch', - 'observer::_sla::_ticket_rebuild_escalation' + 'observer::_sla::_ticket_rebuild_escalation', + 'observer::_transaction' # REST api path config.api_path = '/api/v1' diff --git a/db/migrate/20160415000001_add_slack_integration.rb b/db/migrate/20160415000001_add_slack_integration.rb new file mode 100644 index 000000000..c9fefc581 --- /dev/null +++ b/db/migrate/20160415000001_add_slack_integration.rb @@ -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 diff --git a/db/seeds.rb b/db/seeds.rb index 1d87da6a3..f13859168 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1600,7 +1600,7 @@ Setting.create_if_not_exists( Setting.create_if_not_exists( title: 'Icinga integration', name: 'icinga_integration', - area: 'Integration::Icinga', + area: 'Integration::Switch', description: 'Define if Icinga (http://www.icinga.org) is enabled or not.', options: { form: [ @@ -1632,6 +1632,7 @@ Setting.create_if_not_exists( null: false, name: 'icinga_sender', tag: 'input', + placeholder: 'icinga@monitoring.example.com', }, ], }, @@ -1685,7 +1686,7 @@ Setting.create_if_not_exists( Setting.create_if_not_exists( title: 'Nagios integration', name: 'nagios_integration', - area: 'Integration::Nagios', + area: 'Integration::Switch', description: 'Define if Nagios (http://www.nagios.org) is enabled or not.', options: { form: [ @@ -1717,6 +1718,7 @@ Setting.create_if_not_exists( null: false, name: 'nagios_sender', tag: 'input', + placeholder: 'nagios@monitoring.example.com', }, ], }, @@ -1767,6 +1769,59 @@ Setting.create_if_not_exists( preferences: { prio: 4 }, 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( id: 1, diff --git a/lib/notification_factory.rb b/lib/notification_factory.rb index 0311720f5..26de217e3 100644 --- a/lib/notification_factory.rb +++ b/lib/notification_factory.rb @@ -5,8 +5,17 @@ module NotificationFactory result = NotificationFactory.template_read( template: 'password_reset', locale: 'en-us', - format: 'html', # md - type: 'mailer', # slack + format: 'html', + type: 'mailer', + ) + +or + + result = NotificationFactory.template_read( + template: 'ticket_update', + locale: 'en-us', + format: 'md', + type: 'slack', ) returns @@ -56,8 +65,15 @@ returns =begin string = NotificationFactory.application_template_read( - format: 'html', # md - type: 'mailer', # slack + format: 'html', + type: 'mailer', + ) + +or + + string = NotificationFactory.application_template_read( + format: 'md', + type: 'slack', ) returns diff --git a/lib/notification_factory/slack.rb b/lib/notification_factory/slack.rb new file mode 100644 index 000000000..895b2ff05 --- /dev/null +++ b/lib/notification_factory/slack.rb @@ -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 diff --git a/lib/notification_factory/template.rb b/lib/notification_factory/template.rb index e953d1e27..a0244a6ce 100644 --- a/lib/notification_factory/template.rb +++ b/lib/notification_factory/template.rb @@ -11,6 +11,8 @@ class NotificationFactory::Template ERB.new(@template).result(binding) end + # d - data of object + # d('user.firstname', htmlEscape) def d(key, escape = nil) # do validaton, ignore some methodes @@ -45,19 +47,25 @@ class NotificationFactory::Template h placeholder end + # c - config + # c('fqdn', htmlEscape) def c(key, escape = nil) config = Setting.get(key) return config if escape == false || (escape.nil? && !@escape) h config end + # t - translation + # t('yes', htmlEscape) def t(key, escape = nil) translation = Translation.translate(@locale, key) return translation if escape == false || (escape.nil? && !@escape) h translation end - def a(article) + # a_html - article body in html + # a_html(article) + def a_html(article) content_type = d "#{article}.content_type", false if content_type =~ /html/ return d "#{article}.body", false @@ -65,6 +73,19 @@ class NotificationFactory::Template d("#{article}.body", false).text2html 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) return key if !key CGI.escapeHTML(key.to_s) diff --git a/test/integration/slack_test.rb b/test/integration/slack_test.rb new file mode 100644 index 000000000..33320dfdb --- /dev/null +++ b/test/integration/slack_test.rb @@ -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 diff --git a/test/unit/ticket_notification_test.rb b/test/unit/ticket_notification_test.rb index b9cca9088..2193d50ab 100644 --- a/test/unit/ticket_notification_test.rb +++ b/test/unit/ticket_notification_test.rb @@ -864,8 +864,8 @@ class TicketNotificationTest < ActiveSupport::TestCase ticket1.priority = Ticket::Priority.lookup(name: '3 high') ticket1.save - list = EventBuffer.list('notification') - list_objects = Observer::Ticket::Notification.get_uniq_changes(list) + list = EventBuffer.list('transaction') + 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 - #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.save - list = EventBuffer.list('notification') - list_objects = Observer::Ticket::Notification.get_uniq_changes(list) + list = EventBuffer.list('transaction') + 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 - #2 - #3', list_objects[ticket1.id][:changes]['title'][1]) @@ -916,7 +916,7 @@ class TicketNotificationTest < ActiveSupport::TestCase ) assert(ticket1, 'ticket created - ticket notification template') - bg = Observer::Ticket::Notification::BackgroundJob.new( + bg = Transaction::Notification.new( ticket_id: ticket1.id, article_id: article.id, type: 'update', @@ -992,7 +992,7 @@ class TicketNotificationTest < ActiveSupport::TestCase assert_no_match(/pending_till/, result[:body]) assert_no_match(/i18n/, result[:body]) - bg = Observer::Ticket::Notification::BackgroundJob.new( + bg = Transaction::Notification.new( ticket_id: ticket1.id, article_id: article.id, type: 'update',