chat_notification_service.rb 4.86 KB
Newer Older
1 2
# frozen_string_literal: true

3 4 5 6 7
# Base class for Chat notifications services
# This class is not meant to be used directly, but only to inherit from.
class ChatNotificationService < Service
  include ChatMessage

8 9 10 11 12 13 14
  SUPPORTED_EVENTS = %w[
    push issue confidential_issue merge_request note confidential_note
    tag_push pipeline wiki_page deployment
  ].freeze

  EVENT_CHANNEL = proc { |event| "#{event}_channel" }

15 16 17
  default_value_for :category, 'chat'

  prop_accessor :webhook, :username, :channel
18 19 20 21

  # Custom serialized properties initialization
  prop_accessor(*SUPPORTED_EVENTS.map { |event| EVENT_CHANNEL[event] })

22
  boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
23

24
  validates :webhook, presence: true, public_url: true, if: :activated?
25 26 27 28 29

  def initialize_properties
    if properties.nil?
      self.properties = {}
      self.notify_only_broken_pipelines = true
30
      self.notify_only_default_branch = true
31 32 33
    end
  end

34 35 36 37 38 39 40 41
  def confidential_issue_channel
    properties['confidential_issue_channel'].presence || properties['issue_channel']
  end

  def confidential_note_channel
    properties['confidential_note_channel'].presence || properties['note_channel']
  end

42
  def self.supported_events
43
    SUPPORTED_EVENTS
44 45
  end

46 47 48 49 50 51
  def fields
    default_fields + build_event_channels
  end

  def default_fields
    [
52
      { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}", required: true },
53 54
      { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
      { type: 'checkbox', name: 'notify_only_broken_pipelines' },
55
      { type: 'checkbox', name: 'notify_only_default_branch' }
56 57 58
    ]
  end

59 60 61 62 63 64
  def execute(data)
    return unless supported_events.include?(data[:object_kind])
    return unless webhook.present?

    object_kind = data[:object_kind]

Tiago Botelho's avatar
Tiago Botelho committed
65
    data = custom_data(data)
66 67 68 69 70 71 72 73 74

    # WebHook events often have an 'update' event that follows a 'open' or
    # 'close' action. Ignore update events for now to prevent duplicate
    # messages from arriving.

    message = get_message(object_kind, data)

    return false unless message

75 76 77
    event_type = data[:event_type] || object_kind

    channel_name = get_channel_field(event_type).presence || channel
78

79
    opts = {}
80
    opts[:channel] = channel_name if channel_name
81
    opts[:username] = username if username
82

Tiago Botelho's avatar
Tiago Botelho committed
83
    return false unless notify(message, opts)
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

    true
  end

  def event_channel_names
    supported_events.map { |event| event_channel_name(event) }
  end

  def event_field(event)
    fields.find { |field| field[:name] == event_channel_name(event) }
  end

  def global_fields
    fields.reject { |field| field[:name].end_with?('channel') }
  end

100
  def default_channel_placeholder
101 102 103 104 105
    raise NotImplementedError
  end

  private

Tiago Botelho's avatar
Tiago Botelho committed
106 107 108 109 110 111 112 113 114 115 116 117
  def notify(message, opts)
    Slack::Notifier.new(webhook, opts).ping(
      message.pretext,
      attachments: message.attachments,
      fallback: message.fallback
    )
  end

  def custom_data(data)
    data.merge(project_url: project_url, project_name: project_name)
  end

118 119 120
  def get_message(object_kind, data)
    case object_kind
    when "push", "tag_push"
121
      ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
122
    when "issue"
123
      ChatMessage::IssueMessage.new(data) unless update?(data)
124
    when "merge_request"
125
      ChatMessage::MergeMessage.new(data) unless update?(data)
126
    when "note"
127
      ChatMessage::NoteMessage.new(data)
128
    when "pipeline"
129
      ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
130
    when "wiki_page"
131
      ChatMessage::WikiPageMessage.new(data)
132 133
    when "deployment"
      ChatMessage::DeploymentMessage.new(data)
134 135 136 137 138
    end
  end

  def get_channel_field(event)
    field_name = event_channel_name(event)
139
    self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
140 141 142 143
  end

  def build_event_channels
    supported_events.reduce([]) do |channels, event|
144
      channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder }
145 146 147 148
    end
  end

  def event_channel_name(event)
149
    EVENT_CHANNEL[event]
150 151 152
  end

  def project_name
153
    project.full_name.gsub(/\s/, '')
154 155 156 157 158 159
  end

  def project_url
    project.web_url
  end

160
  def update?(data)
161 162 163 164
    data[:object_attributes][:action] == 'update'
  end

  def should_pipeline_be_notified?(data)
165
    notify_for_ref?(data) && notify_for_pipeline?(data)
166 167
  end

168
  def notify_for_ref?(data)
169
    return true if data[:object_kind] == 'tag_push'
170
    return true if data.dig(:object_attributes, :tag)
171
    return true unless notify_only_default_branch?
172

173 174 175 176 177 178 179
    ref = if data[:ref]
            Gitlab::Git.ref_name(data[:ref])
          else
            data.dig(:object_attributes, :ref)
          end

    ref == project.default_branch
180 181
  end

182
  def notify_for_pipeline?(data)
183 184 185 186 187 188 189 190 191 192
    case data[:object_attributes][:status]
    when 'success'
      !notify_only_broken_pipelines?
    when 'failed'
      true
    else
      false
    end
  end
end