2018-11-20 20:47:30 +05:30
# frozen_string_literal: true
2017-08-17 22:00:37 +05:30
# Base class for Chat notifications services
# This class is not meant to be used directly, but only to inherit from.
2021-06-08 01:23:25 +05:30
class ChatNotificationService < Integration
2017-08-17 22:00:37 +05:30
include ChatMessage
2019-12-04 20:38:33 +05:30
include NotificationBranchSelection
SUPPORTED_EVENTS = %w[
push issue confidential_issue merge_request note confidential_note
tag_push pipeline wiki_page deployment
] . freeze
2021-03-11 19:13:27 +05:30
SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[ issue confidential_issue merge_request note confidential_note ] . freeze
2019-12-04 20:38:33 +05:30
EVENT_CHANNEL = proc { | event | " #{ event } _channel " }
2017-08-17 22:00:37 +05:30
2021-06-08 01:23:25 +05:30
LABEL_NOTIFICATION_BEHAVIOURS = [
MATCH_ANY_LABEL = 'match_any' ,
MATCH_ALL_LABELS = 'match_all'
] . freeze
2017-08-17 22:00:37 +05:30
default_value_for :category , 'chat'
2021-06-08 01:23:25 +05:30
prop_accessor :webhook , :username , :channel , :branches_to_be_notified , :labels_to_be_notified , :labels_to_be_notified_behavior
2019-12-04 20:38:33 +05:30
# Custom serialized properties initialization
prop_accessor ( * SUPPORTED_EVENTS . map { | event | EVENT_CHANNEL [ event ] } )
2017-08-17 22:00:37 +05:30
boolean_accessor :notify_only_broken_pipelines , :notify_only_default_branch
2018-11-08 19:23:39 +05:30
validates :webhook , presence : true , public_url : true , if : :activated?
2021-06-08 01:23:25 +05:30
validates :labels_to_be_notified_behavior , inclusion : { in : LABEL_NOTIFICATION_BEHAVIOURS } , allow_blank : true
2017-08-17 22:00:37 +05:30
def initialize_properties
if properties . nil?
self . properties = { }
self . notify_only_broken_pipelines = true
2019-12-04 20:38:33 +05:30
self . branches_to_be_notified = " default "
2021-06-08 01:23:25 +05:30
self . labels_to_be_notified_behavior = MATCH_ANY_LABEL
2019-12-04 20:38:33 +05:30
elsif ! self . notify_only_default_branch . nil?
# In older versions, there was only a boolean property named
# `notify_only_default_branch`. Now we have a string property named
# `branches_to_be_notified`. Instead of doing a background migration, we
# opted to set a value for the new property based on the old one, if
# users hasn't specified one already. When users edit the service and
# selects a value for this new property, it will override everything.
self . branches_to_be_notified || = notify_only_default_branch? ? " default " : " all "
2017-08-17 22:00:37 +05:30
end
end
2018-04-05 14:03:07 +05:30
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
2017-08-17 22:00:37 +05:30
def self . supported_events
2019-12-04 20:38:33 +05:30
SUPPORTED_EVENTS
2017-08-17 22:00:37 +05:30
end
def fields
default_fields + build_event_channels
end
def default_fields
[
2021-04-29 21:17:54 +05:30
{ type : 'text' , name : 'webhook' , placeholder : " #{ webhook_placeholder } " , required : true } . freeze ,
{ type : 'text' , name : 'username' , placeholder : 'GitLab-integration' } . freeze ,
{ type : 'checkbox' , name : 'notify_only_broken_pipelines' , help : 'Do not send notifications for successful pipelines.' } . freeze ,
2021-03-11 19:13:27 +05:30
{ type : 'select' , name : 'branches_to_be_notified' , choices : branch_choices } . freeze ,
2021-06-08 01:23:25 +05:30
{
type : 'text' ,
name : 'labels_to_be_notified' ,
placeholder : '~backend,~frontend' ,
help : 'Send notifications for issue, merge request, and comment events with the listed labels only. Leave blank to receive notifications for all events.'
} . freeze ,
{
type : 'select' ,
name : 'labels_to_be_notified_behavior' ,
choices : [
[ 'Match any of the labels' , MATCH_ANY_LABEL ] ,
[ 'Match all of the labels' , MATCH_ALL_LABELS ]
]
} . freeze
2020-04-22 19:07:51 +05:30
] . freeze
2017-08-17 22:00:37 +05:30
end
def execute ( data )
return unless supported_events . include? ( data [ :object_kind ] )
2021-03-11 19:13:27 +05:30
return unless notify_label? ( data )
2017-08-17 22:00:37 +05:30
return unless webhook . present?
object_kind = data [ :object_kind ]
data = custom_data ( data )
# 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
2018-04-05 14:03:07 +05:30
event_type = data [ :event_type ] || object_kind
2020-04-22 19:07:51 +05:30
channel_names = get_channel_field ( event_type ) . presence || channel . presence
channels = channel_names & . split ( ',' ) & . map ( & :strip )
2017-08-17 22:00:37 +05:30
opts = { }
2020-04-22 19:07:51 +05:30
opts [ :channel ] = channels if channels . present?
2017-08-17 22:00:37 +05:30
opts [ :username ] = username if username
2021-04-17 20:07:23 +05:30
if notify ( message , opts )
log_usage ( event_type , user_id_from_hook_data ( data ) )
return true
end
2017-08-17 22:00:37 +05:30
2021-04-17 20:07:23 +05:30
false
2017-08-17 22:00:37 +05:30
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
def default_channel_placeholder
raise NotImplementedError
end
private
2021-04-17 20:07:23 +05:30
def log_usage ( _ , _ )
# Implement in child class
end
2021-03-11 19:13:27 +05:30
def labels_to_be_notified_list
return [ ] if labels_to_be_notified . nil?
labels_to_be_notified . delete ( '~' ) . split ( ',' ) . map ( & :strip )
end
def notify_label? ( data )
return true unless SUPPORTED_EVENTS_FOR_LABEL_FILTER . include? ( data [ :object_kind ] ) && labels_to_be_notified . present?
2021-06-08 01:23:25 +05:30
labels = data . dig ( :issue , :labels ) || data . dig ( :merge_request , :labels )
return false if labels . nil?
2021-03-11 19:13:27 +05:30
2021-06-08 01:23:25 +05:30
matching_labels = labels_to_be_notified_list & labels . pluck ( :title )
if labels_to_be_notified_behavior == MATCH_ALL_LABELS
labels_to_be_notified_list . difference ( matching_labels ) . empty?
else
matching_labels . any?
end
2021-03-11 19:13:27 +05:30
end
2021-04-17 20:07:23 +05:30
def user_id_from_hook_data ( data )
data . dig ( :user , :id ) || data [ :user_id ]
end
2019-12-04 20:38:33 +05:30
# every notifier must implement this independently
2017-08-17 22:00:37 +05:30
def notify ( message , opts )
2019-12-04 20:38:33 +05:30
raise NotImplementedError
2017-08-17 22:00:37 +05:30
end
def custom_data ( data )
data . merge ( project_url : project_url , project_name : project_name )
end
def get_message ( object_kind , data )
case object_kind
when " push " , " tag_push "
2021-06-08 01:23:25 +05:30
Integrations :: ChatMessage :: PushMessage . new ( data ) if notify_for_ref? ( data )
2017-08-17 22:00:37 +05:30
when " issue "
2021-06-08 01:23:25 +05:30
Integrations :: ChatMessage :: IssueMessage . new ( data ) unless update? ( data )
2017-08-17 22:00:37 +05:30
when " merge_request "
2021-06-08 01:23:25 +05:30
Integrations :: ChatMessage :: MergeMessage . new ( data ) unless update? ( data )
2017-08-17 22:00:37 +05:30
when " note "
2021-06-08 01:23:25 +05:30
Integrations :: ChatMessage :: NoteMessage . new ( data )
2017-08-17 22:00:37 +05:30
when " pipeline "
2021-06-08 01:23:25 +05:30
Integrations :: ChatMessage :: PipelineMessage . new ( data ) if should_pipeline_be_notified? ( data )
2017-08-17 22:00:37 +05:30
when " wiki_page "
2021-06-08 01:23:25 +05:30
Integrations :: ChatMessage :: WikiPageMessage . new ( data )
2019-07-31 22:56:46 +05:30
when " deployment "
2021-06-08 01:23:25 +05:30
Integrations :: ChatMessage :: DeploymentMessage . new ( data )
2017-08-17 22:00:37 +05:30
end
end
def get_channel_field ( event )
field_name = event_channel_name ( event )
2018-03-17 18:26:18 +05:30
self . public_send ( field_name ) # rubocop:disable GitlabSecurity/PublicSend
2017-08-17 22:00:37 +05:30
end
def build_event_channels
supported_events . reduce ( [ ] ) do | channels , event |
channels << { type : 'text' , name : event_channel_name ( event ) , placeholder : default_channel_placeholder }
end
end
def event_channel_name ( event )
2019-12-04 20:38:33 +05:30
EVENT_CHANNEL [ event ]
2017-08-17 22:00:37 +05:30
end
def project_name
2020-03-13 15:44:24 +05:30
project . full_name
2017-08-17 22:00:37 +05:30
end
def project_url
project . web_url
end
2018-03-17 18:26:18 +05:30
def update? ( data )
2017-08-17 22:00:37 +05:30
data [ :object_attributes ] [ :action ] == 'update'
end
def should_pipeline_be_notified? ( data )
notify_for_ref? ( data ) && notify_for_pipeline? ( data )
end
def notify_for_ref? ( data )
2018-11-08 19:23:39 +05:30
return true if data [ :object_kind ] == 'tag_push'
2018-03-27 19:54:05 +05:30
return true if data . dig ( :object_attributes , :tag )
2019-12-04 20:38:33 +05:30
notify_for_branch? ( data )
2017-08-17 22:00:37 +05:30
end
def notify_for_pipeline? ( data )
case data [ :object_attributes ] [ :status ]
when 'success'
! notify_only_broken_pipelines?
when 'failed'
true
else
false
end
end
end