2021-09-04 01:27:46 +05:30
# frozen_string_literal: true
2022-08-13 15:12:31 +05:30
# Base class for Chat notifications integrations
2021-09-04 01:27:46 +05:30
# This class is not meant to be used directly, but only to inherit from.
module Integrations
class BaseChatNotification < Integration
include ChatMessage
include NotificationBranchSelection
SUPPORTED_EVENTS = %w[
push issue confidential_issue merge_request note confidential_note
2023-03-17 16:20:25 +05:30
tag_push pipeline wiki_page deployment incident
2021-09-04 01:27:46 +05:30
] . freeze
SUPPORTED_EVENTS_FOR_LABEL_FILTER = %w[ issue confidential_issue merge_request note confidential_note ] . freeze
EVENT_CHANNEL = proc { | event | " #{ event } _channel " }
LABEL_NOTIFICATION_BEHAVIOURS = [
MATCH_ANY_LABEL = 'match_any' ,
MATCH_ALL_LABELS = 'match_all'
] . freeze
2023-01-13 00:05:48 +05:30
SECRET_MASK = '************'
2023-04-23 21:23:45 +05:30
CHANNEL_LIMIT_PER_EVENT = 10
2023-01-13 00:05:48 +05:30
attribute :category , default : 'chat'
2021-09-04 01:27:46 +05:30
prop_accessor :webhook , :username , :channel , :branches_to_be_notified , :labels_to_be_notified , :labels_to_be_notified_behavior
# Custom serialized properties initialization
prop_accessor ( * SUPPORTED_EVENTS . map { | event | EVENT_CHANNEL [ event ] } )
boolean_accessor :notify_only_broken_pipelines , :notify_only_default_branch
2023-03-04 22:38:38 +05:30
validates :webhook ,
presence : true ,
public_url : true ,
if : - > ( integration ) { integration . activated? && integration . requires_webhook? }
2023-04-23 21:23:45 +05:30
validates :labels_to_be_notified_behavior , inclusion : { in : LABEL_NOTIFICATION_BEHAVIOURS } , allow_blank : true , if : :activated?
validate :validate_channel_limit , if : :activated?
2021-09-04 01:27:46 +05:30
def initialize_properties
2022-06-21 17:19:12 +05:30
super
if properties . empty?
2021-09-04 01:27:46 +05:30
self . notify_only_broken_pipelines = true
self . branches_to_be_notified = " default "
self . labels_to_be_notified_behavior = MATCH_ANY_LABEL
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
2022-08-13 15:12:31 +05:30
# users haven't specified one already. When users edit the integration and
2021-09-04 01:27:46 +05:30
# select a value for this new property, it will override everything.
self . branches_to_be_notified || = notify_only_default_branch? ? " default " : " all "
end
end
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
def self . supported_events
SUPPORTED_EVENTS
end
def fields
default_fields + build_event_channels
end
def default_fields
[
2023-03-17 16:20:25 +05:30
{
type : 'checkbox' ,
section : SECTION_TYPE_CONFIGURATION ,
name : 'notify_only_broken_pipelines' ,
help : 'Do not send notifications for successful pipelines.'
} . freeze ,
2021-12-11 22:18:48 +05:30
{
type : 'select' ,
2023-03-17 16:20:25 +05:30
section : SECTION_TYPE_CONFIGURATION ,
2021-12-11 22:18:48 +05:30
name : 'branches_to_be_notified' ,
title : s_ ( 'Integrations|Branches for which notifications are to be sent' ) ,
2022-08-13 15:12:31 +05:30
choices : self . class . branch_choices
2021-12-11 22:18:48 +05:30
} . freeze ,
2021-09-04 01:27:46 +05:30
{
type : 'text' ,
2023-03-17 16:20:25 +05:30
section : SECTION_TYPE_CONFIGURATION ,
2021-09-04 01:27:46 +05:30
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' ,
2023-03-17 16:20:25 +05:30
section : SECTION_TYPE_CONFIGURATION ,
2021-09-04 01:27:46 +05:30
name : 'labels_to_be_notified_behavior' ,
choices : [
[ 'Match any of the labels' , MATCH_ANY_LABEL ] ,
[ 'Match all of the labels' , MATCH_ALL_LABELS ]
]
} . freeze
2023-03-04 22:38:38 +05:30
] . tap do | fields |
next unless requires_webhook?
fields . unshift (
{ type : 'text' , name : 'webhook' , help : webhook_help , required : true } . freeze ,
{ type : 'text' , name : 'username' , placeholder : 'GitLab-integration' } . freeze
)
end . freeze
2021-09-04 01:27:46 +05:30
end
def execute ( data )
object_kind = data [ :object_kind ]
2023-03-04 22:38:38 +05:30
return false unless should_execute? ( object_kind )
2021-09-04 01:27:46 +05:30
data = custom_data ( data )
2023-03-04 22:38:38 +05:30
return false unless notify_label? ( data )
2021-09-04 01:27:46 +05:30
# 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
2023-04-23 21:23:45 +05:30
event = data [ :event_type ] || object_kind
channels = channels_for_event ( event )
2021-09-04 01:27:46 +05:30
opts = { }
opts [ :channel ] = channels if channels . present?
opts [ :username ] = username if username
if notify ( message , opts )
2023-04-23 21:23:45 +05:30
log_usage ( event , user_id_from_hook_data ( data ) )
2021-09-04 01:27:46 +05:30
return true
end
false
end
def event_channel_names
2022-08-13 15:12:31 +05:30
return [ ] unless configurable_channels?
2021-09-04 01:27:46 +05:30
2022-08-13 15:12:31 +05:30
supported_events . map { | event | event_channel_name ( event ) }
2021-09-04 01:27:46 +05:30
end
2022-08-13 15:12:31 +05:30
def form_fields
super . reject { | field | field [ :name ] . end_with? ( 'channel' ) }
2021-09-04 01:27:46 +05:30
end
def default_channel_placeholder
raise NotImplementedError
end
2023-01-13 00:05:48 +05:30
def webhook_help
2022-07-16 23:28:13 +05:30
raise NotImplementedError
end
2022-08-13 15:12:31 +05:30
# With some integrations the webhook is already tied to a specific channel,
# for others the channels are configurable for each event.
def configurable_channels?
false
end
def event_channel_name ( event )
EVENT_CHANNEL [ event ]
end
def event_channel_value ( event )
field_name = event_channel_name ( event )
self . public_send ( field_name ) # rubocop:disable GitlabSecurity/PublicSend
end
2023-03-04 22:38:38 +05:30
def requires_webhook?
true
end
2021-09-04 01:27:46 +05:30
private
2023-03-04 22:38:38 +05:30
def should_execute? ( object_kind )
supported_events . include? ( object_kind ) &&
( ! requires_webhook? || webhook . present? )
end
2021-09-04 01:27:46 +05:30
def log_usage ( _ , _ )
# Implement in child class
end
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?
labels = data [ :labels ] || data . dig ( :issue , :labels ) || data . dig ( :merge_request , :labels ) || data . dig ( :object_attributes , :labels )
return false if labels . blank?
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
end
def user_id_from_hook_data ( data )
data . dig ( :user , :id ) || data [ :user_id ]
end
# every notifier must implement this independently
def notify ( message , opts )
raise NotImplementedError
end
def custom_data ( data )
data . merge ( project_url : project_url , project_name : project_name ) . with_indifferent_access
end
2023-03-17 16:20:25 +05:30
# rubocop:disable Metrics/CyclomaticComplexity
2021-09-04 01:27:46 +05:30
def get_message ( object_kind , data )
case object_kind
when " push " , " tag_push "
Integrations :: ChatMessage :: PushMessage . new ( data ) if notify_for_ref? ( data )
when " issue "
Integrations :: ChatMessage :: IssueMessage . new ( data ) unless update? ( data )
when " merge_request "
Integrations :: ChatMessage :: MergeMessage . new ( data ) unless update? ( data )
when " note "
Integrations :: ChatMessage :: NoteMessage . new ( data )
when " pipeline "
Integrations :: ChatMessage :: PipelineMessage . new ( data ) if should_pipeline_be_notified? ( data )
when " wiki_page "
Integrations :: ChatMessage :: WikiPageMessage . new ( data )
when " deployment "
2022-03-02 08:16:31 +05:30
Integrations :: ChatMessage :: DeploymentMessage . new ( data ) if notify_for_ref? ( data )
2023-03-17 16:20:25 +05:30
when " incident "
Integrations :: ChatMessage :: IssueMessage . new ( data ) unless update? ( data )
2021-09-04 01:27:46 +05:30
end
end
2023-03-17 16:20:25 +05:30
# rubocop:enable Metrics/CyclomaticComplexity
2021-09-04 01:27:46 +05:30
def build_event_channels
2022-08-13 15:12:31 +05:30
event_channel_names . map do | channel_field |
{ type : 'text' , name : channel_field , placeholder : default_channel_placeholder }
2021-09-04 01:27:46 +05:30
end
end
def project_name
project . full_name
end
def project_url
project . web_url
end
def update? ( data )
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 )
return true if data [ :object_kind ] == 'tag_push'
2022-03-02 08:16:31 +05:30
ref = data [ :ref ] || data . dig ( :object_attributes , :ref )
return true if ref . blank? # No need to check protected branches when there is no ref
2022-07-23 23:45:48 +05:30
return true if Gitlab :: Git . tag_ref? ( project . repository . expand_ref ( ref ) || ref ) # Skip protected branch check because it doesn't support tags
2021-09-04 01:27:46 +05:30
notify_for_branch? ( data )
end
def notify_for_pipeline? ( data )
case data [ :object_attributes ] [ :status ]
when 'success'
! notify_only_broken_pipelines?
when 'failed'
true
else
false
end
end
2023-04-23 21:23:45 +05:30
def channels_for_event ( event )
channel_names = event_channel_value ( event ) . presence || channel . presence
return [ ] unless channel_names
channel_names . split ( ',' ) . map ( & :strip ) . uniq
end
def unique_channels
@unique_channels || = supported_events . flat_map do | event |
channels_for_event ( event )
end . uniq
end
def validate_channel_limit
supported_events . each do | event |
count = channels_for_event ( event ) . count
next unless count > CHANNEL_LIMIT_PER_EVENT
errors . add (
event_channel_name ( event ) . to_sym ,
format (
s_ ( 'SlackIntegration|cannot have more than %{limit} channels' ) ,
limit : CHANNEL_LIMIT_PER_EVENT
)
)
end
end
2021-09-04 01:27:46 +05:30
end
end
2021-11-11 11:23:49 +05:30
Integrations :: BaseChatNotification . prepend_mod_with ( 'Integrations::BaseChatNotification' )