diff --git a/assets/javascripts/admin/controllers/admin-plugins-chat-provider.js.es6 b/assets/javascripts/admin/controllers/admin-plugins-chat-provider.js.es6 index 09d3a34..e1dd35a 100644 --- a/assets/javascripts/admin/controllers/admin-plugins-chat-provider.js.es6 +++ b/assets/javascripts/admin/controllers/admin-plugins-chat-provider.js.es6 @@ -8,6 +8,16 @@ import { popupAjaxError } from 'discourse/lib/ajax-error'; export default Ember.Controller.extend({ modalShowing: false, + anyErrors: function(){ + var anyErrors = false; + this.get('model.rules').forEach(function(rule){ + if(rule.error_key){ + anyErrors = true; + } + }); + return anyErrors; + }.property('model.rules'), + actions:{ create(){ this.set('modalShowing', true); @@ -25,6 +35,9 @@ export default Ember.Controller.extend({ self.send('refresh'); }).catch(popupAjaxError) }, + showError(error_key){ + bootbox.alert(I18n.t(error_key)); + } } diff --git a/assets/javascripts/admin/models/rule.js.es6 b/assets/javascripts/admin/models/rule.js.es6 index c58fc04..2ee1523 100644 --- a/assets/javascripts/admin/models/rule.js.es6 +++ b/assets/javascripts/admin/models/rule.js.es6 @@ -14,6 +14,7 @@ export default RestModel.extend({ provider: '', channel: '', filter: null, + error_key: null, @computed('category_id') category(categoryId) { diff --git a/assets/javascripts/discourse/templates/admin/plugins-chat-provider.hbs b/assets/javascripts/discourse/templates/admin/plugins-chat-provider.hbs index 6c10e92..2fcd602 100644 --- a/assets/javascripts/discourse/templates/admin/plugins-chat-provider.hbs +++ b/assets/javascripts/discourse/templates/admin/plugins-chat-provider.hbs @@ -1,4 +1,10 @@ + {{#if anyErrors}} +
+ + {{i18n "chat_integration.rules_with_errors"}} +
+ {{/if}} @@ -16,10 +22,19 @@ + {{#each model.rules as |rule|}} - + {{/each}} diff --git a/assets/stylesheets/chat-integration-admin.scss b/assets/stylesheets/chat-integration-admin.scss index 27a6276..d5aaad3 100644 --- a/assets/stylesheets/chat-integration-admin.scss +++ b/assets/stylesheets/chat-integration-admin.scss @@ -13,6 +13,19 @@ margin-top: 10px; } + div.error { + font-size: 1.1em; + font-weight:bold; + max-width: 100%; + margin-top: 10px; + margin-bottom: 10px; + background-color: dark-light-diff($quaternary, $secondary, 70%, -70%); + padding: 15px; + + + } + + } #chat_integration_edit_rule_modal{ diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 93266fc..10e374c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -4,6 +4,8 @@ en: menu_title: "Chat Integrations" settings: "Settings" no_providers: "You need to enable some providers in the plugin settings" + rules_with_errors: "Some rules for this provider failed last time they were triggered. Click the error icon(s) to learn more." + rule_exception: "An unknown error occured when this rule was last triggered. Check the site logs for more information." all_categories: "(all categories)" all_tags: "(all tags)" create_rule: "Create Rule" @@ -43,6 +45,9 @@ en: slack: title: "Slack" channel_instructions: "e.g. #channel, @username." + errors: + action_prohibited: "The bot does not have permission to post to that channel" + channel_not_found: "The specified channel does not exist on slack" ####################################### ########## TELEGRAM STRINGS ########### diff --git a/lib/discourse_chat/manager.rb b/lib/discourse_chat/manager.rb index b9952d9..76b8098 100644 --- a/lib/discourse_chat/manager.rb +++ b/lib/discourse_chat/manager.rb @@ -61,11 +61,30 @@ module DiscourseChat # Loop through each rule, and trigger appropriate notifications matching_rules.each do |rule| - Rails.logger.info("Sending notification to provider #{rule.provider}, channel #{rule.channel}") provider = ::DiscourseChat::Provider.get_by_name(rule.provider) is_enabled = ::DiscourseChat::Provider.is_enabled(provider) + if provider and is_enabled - provider.trigger_notification(post, rule.channel) + begin + provider.trigger_notification(post, rule.channel) + rule.update({error_key: nil}, false) if rule.error_key + rescue => e + if e.class == DiscourseChat::ProviderError and e.info.key?(:error_key) and !e.info[:error_key].nil? + rule.error_key = e.info[:error_key] + else + rule.error_key = 'chat_integration.rule_exception' + end + rule.save(false) # Save without validations + + # Log the error + Discourse.handle_job_exception(e, + message: "Triggering notifications failed", + extra: { provider_name: provider::PROVIDER_NAME, + channel: rule.channel, + post_id: post.id, + error_info: e.class == DiscourseChat::ProviderError ? e.info : nil } + ) + end elsif provider # Provider is disabled, don't do anything else diff --git a/lib/discourse_chat/provider.rb b/lib/discourse_chat/provider.rb index 41119a4..364bca5 100644 --- a/lib/discourse_chat/provider.rb +++ b/lib/discourse_chat/provider.rb @@ -1,4 +1,13 @@ module DiscourseChat + class ProviderError < StandardError + attr_accessor :info + + def initialize(message = nil, info: nil) + super(message) + self.info = info.nil? ? {} : info + end + end + module Provider def self.providers constants.select do |constant| diff --git a/lib/discourse_chat/provider/slack/slack_provider.rb b/lib/discourse_chat/provider/slack/slack_provider.rb index 4800361..a8674df 100644 --- a/lib/discourse_chat/provider/slack/slack_provider.rb +++ b/lib/discourse_chat/provider/slack/slack_provider.rb @@ -105,7 +105,19 @@ module DiscourseChat::Provider::SlackProvider req = Net::HTTP::Post.new(URI(SiteSetting.chat_integration_slack_outbound_webhook_url), 'Content-Type' =>'application/json') req.body = message.to_json response = http.request(req) - response + + unless response.kind_of? Net::HTTPSuccess + if response.code.to_s == '403' + error_key = 'chat_integration.provider.slack.errors.action_prohibited' + elsif response.body == 'channel_not_found' or response.body == 'channel_is_archived' + error_key = 'chat_integration.provider.slack.errors.channel_not_found' + else + error_key = nil + end + raise ::DiscourseChat::ProviderError.new info: {error_key: error_key, request: req.body, response_code:response.code, response_body:response.body} + end + + end def self.trigger_notification(post, channel) diff --git a/lib/discourse_chat/rule.rb b/lib/discourse_chat/rule.rb index d8355ac..03d82b5 100644 --- a/lib/discourse_chat/rule.rb +++ b/lib/discourse_chat/rule.rb @@ -2,7 +2,7 @@ # Using this means we can use a standard serializer for sending JSON to the client, and also have convenient save/update/delete methods # Since this is now being used in two plugins, maybe it should be built into core somehow class DiscourseChat::Rule - attr_accessor :id, :provider, :channel, :category_id, :tags, :filter + attr_accessor :id, :provider, :channel, :category_id, :tags, :filter, :error_key def initialize(h={}) @provider = '' @@ -10,6 +10,7 @@ @category_id = nil @tags = [] @filter = 'watch' + @error_key = nil h.each {|k,v| public_send("#{k}=",v)} end @@ -41,7 +42,7 @@ def self.from_hash(h) rule = DiscourseChat::Rule.new - [:provider, :channel, :category_id, :tags, :filter].each do |sym| + [:provider, :channel, :category_id, :tags, :filter, :error_key].each do |sym| rule.send("#{sym}=", h[sym]) if h[sym] end if h[:id] @@ -58,6 +59,7 @@ category_id: @category_id, tags: @tags, filter: @filter, + error_key: @error_key, } end @@ -70,16 +72,18 @@ from_hash hash end - def update(h) - [:provider, :channel, :category_id, :tags, :filter].each do |sym| - public_send("#{sym}=", h[sym]) if h[sym] + def update(h, validate=true) + [:provider, :channel, :category_id, :tags, :filter, :error_key].each do |sym| + public_send("#{sym}=", h[sym]) if h.key?(sym) end - save + save(validate) end - def save - return false if not valid? + def save(validate=true) + if validate + return false if not valid? + end unless @id && @id > 0 @id = self.class.alloc_id @@ -88,8 +92,8 @@ return self end - def save! - if not save + def save!(validate=true) + if not save(validate) raise 'Rule not valid' end return self diff --git a/plugin.rb b/plugin.rb index 57ebf47..14999c3 100644 --- a/plugin.rb +++ b/plugin.rb @@ -43,6 +43,7 @@ after_initialize do module ::Jobs class NotifyChats < Jobs::Base + sidekiq_options retry: false # Don't retry, could result in duplicate notifications for some providers def execute(args) return if not SiteSetting.chat_integration_enabled? # Plugin may have been disabled since job triggered @@ -113,6 +114,7 @@ after_initialize do def update_rule begin rule = DiscourseChat::Rule.find(params[:id].to_i) + rule.error_key = nil # Reset any error on the rule hash = params.require(:rule) if not rule.update(hash) @@ -135,7 +137,7 @@ after_initialize do end class DiscourseChat::RuleSerializer < ActiveModel::Serializer - attributes :id, :provider, :channel, :category_id, :tags, :filter + attributes :id, :provider, :channel, :category_id, :tags, :filter, :error_key end require_dependency 'admin_constraint' diff --git a/spec/lib/discourse_chat/manager_spec.rb b/spec/lib/discourse_chat/manager_spec.rb index 78157ea..1a910e7 100644 --- a/spec/lib/discourse_chat/manager_spec.rb +++ b/spec/lib/discourse_chat/manager_spec.rb @@ -19,14 +19,22 @@ RSpec.describe DiscourseChat::Manager do PROVIDER_NAME = "dummy".freeze PROVIDER_ENABLED_SETTING = :chat_integration_enabled # Tie to main plugin enabled setting @@sent_messages = [] + @@raise_exception = nil def self.trigger_notification(post, channel) + if @@raise_exception + raise @@raise_exception + end @@sent_messages.push(post: post.id, channel: channel) end def self.sent_messages @@sent_messages end + + def self.set_raise_exception(bool) + @@raise_exception = bool + end end end @@ -40,7 +48,28 @@ RSpec.describe DiscourseChat::Manager do DiscourseChat::Rule.new({provider: provider, channel: channel, filter:filter, category_id:category_id, tags:tags}).save! end - it "should only send notifications when provider is enabled" do + it "should fail gracefully when a provider throws an exception" do + create_rule('dummy', 'chan1', 'watch', category.id, nil) + + # Triggering a ProviderError should set the error_key to the error message + ::DiscourseChat::Provider::DummyProvider.set_raise_exception(DiscourseChat::ProviderError.new info: {error_key:"hello"}) + manager.trigger_notifications(first_post.id) + expect(provider.sent_messages.map{|x| x[:channel]}).to contain_exactly() + expect(DiscourseChat::Rule.all.first.error_key).to eq('hello') + + # Triggering a different error should set the error_key to a generic message + ::DiscourseChat::Provider::DummyProvider.set_raise_exception(StandardError.new "hello") + manager.trigger_notifications(first_post.id) + expect(provider.sent_messages.map{|x| x[:channel]}).to contain_exactly() + expect(DiscourseChat::Rule.all.first.error_key).to eq('chat_integration.rule_exception') + + ::DiscourseChat::Provider::DummyProvider.set_raise_exception(nil) + + manager.trigger_notifications(first_post.id) + expect(DiscourseChat::Rule.all.first.error_key.nil?).to be true + end + + it "should not send notifications when provider is disabled" do SiteSetting.chat_integration_enabled = false create_rule('dummy', 'chan1', 'watch', category.id, nil)
{{rule.channel}} + {{#if rule.error_key}} + + {{d-button action="showError" actionParam=rule.error_key class="delete btn-danger" icon="exclamation-triangle"}} + + {{/if}} + + {{rule.channel}} + {{rule.filterName}} @@ -44,7 +59,7 @@ {{d-button action="edit" actionParam=rule icon="pencil" class="edit" title="chat_integration.rule_table.edit_rule"}} - {{d-button action="delete" actionParam=rule icon="trash-o" class="delete btn-danger" title="chat_integration.rule_table.delete_rule"}} + {{d-button action="delete" actionParam=rule icon="trash-o" class="delete" title="chat_integration.rule_table.delete_rule"}}