Commit 4675d7da authored by Luke Duncalfe's avatar Luke Duncalfe Committed by Max Woolf

Namespace `ChatMessage` classes as `Integrations::`

parent b2cf103c
...@@ -2941,7 +2941,6 @@ Style/RegexpLiteralMixedPreserve: ...@@ -2941,7 +2941,6 @@ Style/RegexpLiteralMixedPreserve:
- 'app/models/concerns/ci/maskable.rb' - 'app/models/concerns/ci/maskable.rb'
- 'app/models/operations/feature_flag.rb' - 'app/models/operations/feature_flag.rb'
- 'app/models/packages/go/module.rb' - 'app/models/packages/go/module.rb'
- 'app/models/project_services/chat_message/base_message.rb'
- 'app/services/packages/conan/search_service.rb' - 'app/services/packages/conan/search_service.rb'
- 'app/services/projects/update_remote_mirror_service.rb' - 'app/services/projects/update_remote_mirror_service.rb'
- 'config/initializers/rspec_profiling.rb' - 'config/initializers/rspec_profiling.rb'
......
...@@ -598,7 +598,6 @@ Rails/RenderInline: ...@@ -598,7 +598,6 @@ Rails/RenderInline:
# SupportedStyles: conservative, aggressive # SupportedStyles: conservative, aggressive
Rails/ShortI18n: Rails/ShortI18n:
Exclude: Exclude:
- 'app/models/project_services/chat_message/pipeline_message.rb'
- 'app/uploaders/content_type_whitelist.rb' - 'app/uploaders/content_type_whitelist.rb'
# Offense count: 1144 # Offense count: 1144
......
# frozen_string_literal: true
module Integrations
module ChatMessage
class AlertMessage < BaseMessage
attr_reader :title
attr_reader :alert_url
attr_reader :severity
attr_reader :events
attr_reader :status
attr_reader :started_at
def initialize(params)
@project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
@project_url = params.dig(:project, :web_url) || params[:project_url]
@title = params.dig(:object_attributes, :title)
@alert_url = params.dig(:object_attributes, :url)
@severity = params.dig(:object_attributes, :severity)
@events = params.dig(:object_attributes, :events)
@status = params.dig(:object_attributes, :status)
@started_at = params.dig(:object_attributes, :started_at)
end
def attachments
[{
title: title,
title_link: alert_url,
color: attachment_color,
fields: attachment_fields
}]
end
def message
"Alert firing in #{project_name}"
end
private
def attachment_color
"#C95823"
end
def attachment_fields
[
{
title: "Severity",
value: severity.to_s.humanize,
short: true
},
{
title: "Events",
value: events,
short: true
},
{
title: "Status",
value: status.to_s.humanize,
short: true
},
{
title: "Start time",
value: format_time(started_at),
short: true
}
]
end
# This formats time into the following format
# April 23rd, 2020 1:06AM UTC
def format_time(time)
time = Time.zone.parse(time.to_s)
time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z")
end
end
end
end
# frozen_string_literal: true
module Integrations
module ChatMessage
class BaseMessage
RELATIVE_LINK_REGEX = %r{!\[[^\]]*\]\((/uploads/[^\)]*)\)}.freeze
attr_reader :markdown
attr_reader :user_full_name
attr_reader :user_name
attr_reader :user_avatar
attr_reader :project_name
attr_reader :project_url
def initialize(params)
@markdown = params[:markdown] || false
@project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
@project_url = params.dig(:project, :web_url) || params[:project_url]
@user_full_name = params.dig(:user, :name) || params[:user_full_name]
@user_name = params.dig(:user, :username) || params[:user_name]
@user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
end
def user_combined_name
if user_full_name.present?
"#{user_full_name} (#{user_name})"
else
user_name
end
end
def summary
return message if markdown
format(message)
end
def pretext
summary
end
def fallback
format(message)
end
def attachments
raise NotImplementedError
end
def activity
raise NotImplementedError
end
private
def message
raise NotImplementedError
end
def format(string)
Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
end
def format_relative_links(string)
string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1")
end
def attachment_color
'#345'
end
def link(text, url)
"[#{text}](#{url})"
end
def pretty_duration(seconds)
parse_string =
if duration < 1.hour
'%M:%S'
else
'%H:%M:%S'
end
Time.at(seconds).utc.strftime(parse_string)
end
end
end
end
# frozen_string_literal: true
module Integrations
module ChatMessage
class DeploymentMessage < BaseMessage
attr_reader :commit_title
attr_reader :commit_url
attr_reader :deployable_id
attr_reader :deployable_url
attr_reader :environment
attr_reader :short_sha
attr_reader :status
attr_reader :user_url
def initialize(data)
super
@commit_title = data[:commit_title]
@commit_url = data[:commit_url]
@deployable_id = data[:deployable_id]
@deployable_url = data[:deployable_url]
@environment = data[:environment]
@short_sha = data[:short_sha]
@status = data[:status]
@user_url = data[:user_url]
end
def attachments
[{
text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}",
color: color
}]
end
def activity
{}
end
private
def message
if running?
"Starting deploy to #{environment}"
else
"Deploy to #{environment} #{humanized_status}"
end
end
def color
case status
when 'success'
'good'
when 'canceled'
'warning'
when 'failed'
'danger'
else
'#334455'
end
end
def project_link
link(project_name, project_url)
end
def deployment_link
link("##{deployable_id}", deployable_url)
end
def user_link
link(user_combined_name, user_url)
end
def commit_link
link(short_sha, commit_url)
end
def humanized_status
status == 'success' ? 'succeeded' : status
end
def running?
status == 'running'
end
end
end
end
# frozen_string_literal: true
module Integrations
module ChatMessage
class IssueMessage < BaseMessage
attr_reader :title
attr_reader :issue_iid
attr_reader :issue_url
attr_reader :action
attr_reader :state
attr_reader :description
def initialize(params)
super
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@title = obj_attr[:title]
@issue_iid = obj_attr[:iid]
@issue_url = obj_attr[:url]
@action = obj_attr[:action]
@state = obj_attr[:state]
@description = obj_attr[:description] || ''
end
def attachments
return [] unless opened_issue?
return description if markdown
description_message
end
def activity
{
title: "Issue #{state} by #{user_combined_name}",
subtitle: "in #{project_link}",
text: issue_link,
image: user_avatar
}
end
private
def message
"[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
end
def opened_issue?
action == 'open'
end
def description_message
[{
title: issue_title,
title_link: issue_url,
text: format(description),
color: '#C95823'
}]
end
def project_link
link(project_name, project_url)
end
def issue_link
link(issue_title, issue_url)
end
def issue_title
"#{Issue.reference_prefix}#{issue_iid} #{title}"
end
end
end
end
# frozen_string_literal: true
module Integrations
module ChatMessage
class MergeMessage < BaseMessage
attr_reader :merge_request_iid
attr_reader :source_branch
attr_reader :target_branch
attr_reader :action
attr_reader :state
attr_reader :title
def initialize(params)
super
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@merge_request_iid = obj_attr[:iid]
@source_branch = obj_attr[:source_branch]
@target_branch = obj_attr[:target_branch]
@action = obj_attr[:action]
@state = obj_attr[:state]
@title = format_title(obj_attr[:title])
end
def attachments
[]
end
def activity
{
title: "Merge request #{state_or_action_text} by #{user_combined_name}",
subtitle: "in #{project_link}",
text: merge_request_link,
image: user_avatar
}
end
private
def format_title(title)
'*' + title.lines.first.chomp + '*'
end
def message
merge_request_message
end
def project_link
link(project_name, project_url)
end
def merge_request_message
"#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}"
end
def merge_request_link
link(merge_request_title, merge_request_url)
end
def merge_request_title
"#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}"
end
def merge_request_url
"#{project_url}/-/merge_requests/#{merge_request_iid}"
end
def state_or_action_text
case action
when 'approved', 'unapproved'
action
when 'approval'
'added their approval to'
when 'unapproval'
'removed their approval from'
else
state
end
end
end
end
end
# frozen_string_literal: true
module Integrations
module ChatMessage
class NoteMessage < BaseMessage
attr_reader :note
attr_reader :note_url
attr_reader :title
attr_reader :target
def initialize(params)
super
params = HashWithIndifferentAccess.new(params)
obj_attr = params[:object_attributes]
@note = obj_attr[:note]
@note_url = obj_attr[:url]
@target, @title = case obj_attr[:noteable_type]
when "Commit"
create_commit_note(params[:commit])
when "Issue"
create_issue_note(params[:issue])
when "MergeRequest"
create_merge_note(params[:merge_request])
when "Snippet"
create_snippet_note(params[:snippet])
end
end
def attachments
return note if markdown
description_message
end
def activity
{
title: "#{user_combined_name} #{link('commented on ' + target, note_url)}",
subtitle: "in #{project_link}",
text: formatted_title,
image: user_avatar
}
end
private
def message
"#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
end
def format_title(title)
title.lines.first.chomp
end
def formatted_title
format_title(title)
end
def create_issue_note(issue)
["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]]
end
def create_commit_note(commit)
commit_sha = Commit.truncate_sha(commit[:id])
["commit #{commit_sha}", commit[:message]]
end
def create_merge_note(merge_request)
["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]]
end
def create_snippet_note(snippet)
["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]]
end
def description_message
[{ text: format(note), color: attachment_color }]
end
def project_link
link(project_name, project_url)
end
end
end
end
# frozen_string_literal: true
module Integrations
module ChatMessage
class PipelineMessage < BaseMessage
MAX_VISIBLE_JOBS = 10
attr_reader :user
attr_reader :ref_type
attr_reader :ref
attr_reader :status
attr_reader :detailed_status
attr_reader :duration
attr_reader :finished_at
attr_reader :pipeline_id
attr_reader :failed_stages
attr_reader :failed_jobs
attr_reader :project
attr_reader :commit
attr_reader :committer
attr_reader :pipeline
def initialize(data)
super
@user = data[:user]
@user_name = data.dig(:user, :username) || 'API'
pipeline_attributes = data[:object_attributes]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status]
@detailed_status = pipeline_attributes[:detailed_status]
@duration = pipeline_attributes[:duration].to_i
@finished_at = pipeline_attributes[:finished_at] ? Time.parse(pipeline_attributes[:finished_at]).to_i : nil
@pipeline_id = pipeline_attributes[:id]
# Get list of jobs that have actually failed (after exhausting all retries)
@failed_jobs = actually_failed_jobs(Array(data[:builds]))
@failed_stages = @failed_jobs.map { |j| j[:stage] }.uniq
@project = Project.find(data[:project][:id])
@commit = project.commit_by(oid: data[:commit][:id])
@committer = commit.committer
@pipeline = Ci::Pipeline.find(pipeline_id)
end
def pretext
''
end
def attachments
return message if markdown
[{
fallback: format(message),
color: attachment_color,
author_name: user_combined_name,
author_icon: user_avatar,
author_link: author_url,
title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") %
{
pipeline_id: pipeline_id,
humanized_status: humanized_status,
duration: pretty_duration(duration)
},
title_link: pipeline_url,
fields: attachments_fields,
footer: project.name,
footer_icon: project.avatar_url(only_path: false),
ts: finished_at
}]
end
def activity
{
title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") %
{
pipeline_link: pipeline_link,
ref_type: ref_type,
ref_link: ref_link,
user_combined_name: user_combined_name,
humanized_status: humanized_status
},
subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link },
text: s_("ChatMessage|in %{duration}") % { duration: pretty_duration(duration) },
image: user_avatar || ''
}
end
private
def actually_failed_jobs(builds)
succeeded_job_names = builds.map { |b| b[:name] if b[:status] == 'success' }.compact.uniq
failed_jobs = builds.select do |build|
# Select jobs which doesn't have a successful retry
build[:status] == 'failed' && !succeeded_job_names.include?(build[:name])
end
failed_jobs.uniq { |job| job[:name] }.reverse
end
def failed_stages_field
{
title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
short: true
}
end
def failed_jobs_field
{
title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length),
value: Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
short: true
}
end
def yaml_error_field
{
title: s_("ChatMessage|Invalid CI config YAML file"),
value: pipeline.yaml_errors,
short: false
}
end
def attachments_fields
fields = [
{
title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
value: Slack::Messenger::Util::LinkFormatter.format(ref_link),
short: true
},
{
title: s_("ChatMessage|Commit"),
value: Slack::Messenger::Util::LinkFormatter.format(commit_link),
short: true
}
]
fields << failed_stages_field if failed_stages.any?
fields << failed_jobs_field if failed_jobs.any?
fields << yaml_error_field if pipeline.has_yaml_errors?
fields
end
def message
s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
{
project_link: project_link,
pipeline_link: pipeline_link,
ref_type: ref_type,
ref_link: ref_link,
user_combined_name: user_combined_name,
humanized_status: humanized_status,
duration: pretty_duration(duration)
}
end
def humanized_status
case status
when 'success'
detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed")
when 'failed'
s_("ChatMessage|has failed")
else
status
end
end
def attachment_color
case status
when 'success'
detailed_status == 'passed with warnings' ? 'warning' : 'good'
else
'danger'
end
end
def ref_url
if ref_type == 'tag'
"#{project_url}/-/tags/#{ref}"
else
"#{project_url}/-/commits/#{ref}"
end
end
def ref_link
"[#{ref}](#{ref_url})"
end
def project_url
project.web_url
end
def project_link
"[#{project.name}](#{project_url})"
end
def pipeline_failed_jobs_url
"#{project_url}/-/pipelines/#{pipeline_id}/failures"
end
def pipeline_url
if failed_jobs.any?
pipeline_failed_jobs_url
else
"#{project_url}/-/pipelines/#{pipeline_id}"
end
end
def pipeline_link
"[##{pipeline_id}](#{pipeline_url})"
end
def job_url(job)
"#{project_url}/-/jobs/#{job[:id]}"
end
def job_link(job)
"[#{job[:name]}](#{job_url(job)})"
end
def failed_jobs_links
failed = failed_jobs.slice(0, MAX_VISIBLE_JOBS)
truncated = failed_jobs.slice(MAX_VISIBLE_JOBS, failed_jobs.size)
failed_links = failed.map { |job| job_link(job) }
unless truncated.blank?
failed_links << s_("ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})") % {
count: truncated.size,
pipeline_failed_jobs_url: pipeline_failed_jobs_url
}
end
failed_links.join(I18n.t(:'support.array.words_connector'))
end
def stage_link(stage)
# All stages link to the pipeline page
"[#{stage}](#{pipeline_url})"
end
def failed_stages_links
failed_stages.map { |s| stage_link(s) }.join(I18n.t(:'support.array.words_connector'))
end
def commit_url
Gitlab::UrlBuilder.build(commit)
end
def commit_link
"[#{commit.title}](#{commit_url})"
end
def author_url
return unless user && committer
Gitlab::UrlBuilder.build(committer)
end
end
end
end
# frozen_string_literal: true
module Integrations
module ChatMessage
class PushMessage < BaseMessage
attr_reader :after
attr_reader :before
attr_reader :commits
attr_reader :ref
attr_reader :ref_type
def initialize(params)
super
@after = params[:after]
@before = params[:before]
@commits = params.fetch(:commits, [])
@ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
@ref = Gitlab::Git.ref_name(params[:ref])
end
def attachments
return [] if new_branch? || removed_branch?
return commit_messages if markdown
commit_message_attachments
end
def activity
{
title: humanized_action(short: true),
subtitle: "in #{project_link}",
text: compare_link,
image: user_avatar
}
end
private
def humanized_action(short: false)
action, ref_link, target_link = compose_action_details
text = [user_combined_name, action, ref_type, ref_link]
text << target_link unless short
text.join(' ')
end
def message
humanized_action
end
def format(string)
Slack::Messenger::Util::LinkFormatter.format(string)
end
def commit_messages
commits.map { |commit| compose_commit_message(commit) }.join("\n\n")
end
def commit_message_attachments
[{ text: format(commit_messages), color: attachment_color }]
end
def compose_commit_message(commit)
author = commit[:author][:name]
id = Commit.truncate_sha(commit[:id])
title = commit[:title]
url = commit[:url]
"[#{id}](#{url}): #{title} - #{author}"
end
def new_branch?
Gitlab::Git.blank_ref?(before)
end
def removed_branch?
Gitlab::Git.blank_ref?(after)
end
def ref_url
if ref_type == 'tag'
"#{project_url}/-/tags/#{ref}"
else
"#{project_url}/commits/#{ref}"
end
end
def compare_url
"#{project_url}/compare/#{before}...#{after}"
end
def ref_link
"[#{ref}](#{ref_url})"
end
def project_link
"[#{project_name}](#{project_url})"
end
def compare_link
"[Compare changes](#{compare_url})"
end
def compose_action_details
if new_branch?
['pushed new', ref_link, "to #{project_link}"]
elsif removed_branch?
['removed', ref, "from #{project_link}"]
else
['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
end
end
def attachment_color
'#345'
end
end
end
end
# frozen_string_literal: true
module Integrations
module ChatMessage
class WikiPageMessage < BaseMessage
attr_reader :title
attr_reader :wiki_page_url
attr_reader :action
attr_reader :description
def initialize(params)
super
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@title = obj_attr[:title]
@wiki_page_url = obj_attr[:url]
@description = obj_attr[:message]
@action =
case obj_attr[:action]
when "create"
"created"
when "update"
"edited"
end
end
def attachments
return description if markdown
description_message
end
def activity
{
title: "#{user_combined_name} #{action} #{wiki_page_link}",
subtitle: "in #{project_link}",
text: title,
image: user_avatar
}
end
private
def message
"#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
end
def description_message
[{ text: format(@description), color: attachment_color }]
end
def project_link
"[#{project_name}](#{project_url})"
end
def wiki_page_link
"[wiki page](#{wiki_page_url})"
end
end
end
end
# frozen_string_literal: true
module ChatMessage
class AlertMessage < BaseMessage
attr_reader :title
attr_reader :alert_url
attr_reader :severity
attr_reader :events
attr_reader :status
attr_reader :started_at
def initialize(params)
@project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
@project_url = params.dig(:project, :web_url) || params[:project_url]
@title = params.dig(:object_attributes, :title)
@alert_url = params.dig(:object_attributes, :url)
@severity = params.dig(:object_attributes, :severity)
@events = params.dig(:object_attributes, :events)
@status = params.dig(:object_attributes, :status)
@started_at = params.dig(:object_attributes, :started_at)
end
def attachments
[{
title: title,
title_link: alert_url,
color: attachment_color,
fields: attachment_fields
}]
end
def message
"Alert firing in #{project_name}"
end
private
def attachment_color
"#C95823"
end
def attachment_fields
[
{
title: "Severity",
value: severity.to_s.humanize,
short: true
},
{
title: "Events",
value: events,
short: true
},
{
title: "Status",
value: status.to_s.humanize,
short: true
},
{
title: "Start time",
value: format_time(started_at),
short: true
}
]
end
# This formats time into the following format
# April 23rd, 2020 1:06AM UTC
def format_time(time)
time = Time.zone.parse(time.to_s)
time.strftime("%B #{time.day.ordinalize}, %Y %l:%M%p %Z")
end
end
end
# frozen_string_literal: true
module ChatMessage
class BaseMessage
RELATIVE_LINK_REGEX = /!\[[^\]]*\]\((\/uploads\/[^\)]*)\)/.freeze
attr_reader :markdown
attr_reader :user_full_name
attr_reader :user_name
attr_reader :user_avatar
attr_reader :project_name
attr_reader :project_url
def initialize(params)
@markdown = params[:markdown] || false
@project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
@project_url = params.dig(:project, :web_url) || params[:project_url]
@user_full_name = params.dig(:user, :name) || params[:user_full_name]
@user_name = params.dig(:user, :username) || params[:user_name]
@user_avatar = params.dig(:user, :avatar_url) || params[:user_avatar]
end
def user_combined_name
if user_full_name.present?
"#{user_full_name} (#{user_name})"
else
user_name
end
end
def summary
return message if markdown
format(message)
end
def pretext
summary
end
def fallback
format(message)
end
def attachments
raise NotImplementedError
end
def activity
raise NotImplementedError
end
private
def message
raise NotImplementedError
end
def format(string)
Slack::Messenger::Util::LinkFormatter.format(format_relative_links(string))
end
def format_relative_links(string)
string.gsub(RELATIVE_LINK_REGEX, "#{project_url}\\1")
end
def attachment_color
'#345'
end
def link(text, url)
"[#{text}](#{url})"
end
def pretty_duration(seconds)
parse_string =
if duration < 1.hour
'%M:%S'
else
'%H:%M:%S'
end
Time.at(seconds).utc.strftime(parse_string)
end
end
end
# frozen_string_literal: true
module ChatMessage
class DeploymentMessage < BaseMessage
attr_reader :commit_title
attr_reader :commit_url
attr_reader :deployable_id
attr_reader :deployable_url
attr_reader :environment
attr_reader :short_sha
attr_reader :status
attr_reader :user_url
def initialize(data)
super
@commit_title = data[:commit_title]
@commit_url = data[:commit_url]
@deployable_id = data[:deployable_id]
@deployable_url = data[:deployable_url]
@environment = data[:environment]
@short_sha = data[:short_sha]
@status = data[:status]
@user_url = data[:user_url]
end
def attachments
[{
text: "#{project_link} with job #{deployment_link} by #{user_link}\n#{commit_link}: #{commit_title}",
color: color
}]
end
def activity
{}
end
private
def message
if running?
"Starting deploy to #{environment}"
else
"Deploy to #{environment} #{humanized_status}"
end
end
def color
case status
when 'success'
'good'
when 'canceled'
'warning'
when 'failed'
'danger'
else
'#334455'
end
end
def project_link
link(project_name, project_url)
end
def deployment_link
link("##{deployable_id}", deployable_url)
end
def user_link
link(user_combined_name, user_url)
end
def commit_link
link(short_sha, commit_url)
end
def humanized_status
status == 'success' ? 'succeeded' : status
end
def running?
status == 'running'
end
end
end
# frozen_string_literal: true
module ChatMessage
class IssueMessage < BaseMessage
attr_reader :title
attr_reader :issue_iid
attr_reader :issue_url
attr_reader :action
attr_reader :state
attr_reader :description
def initialize(params)
super
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@title = obj_attr[:title]
@issue_iid = obj_attr[:iid]
@issue_url = obj_attr[:url]
@action = obj_attr[:action]
@state = obj_attr[:state]
@description = obj_attr[:description] || ''
end
def attachments
return [] unless opened_issue?
return description if markdown
description_message
end
def activity
{
title: "Issue #{state} by #{user_combined_name}",
subtitle: "in #{project_link}",
text: issue_link,
image: user_avatar
}
end
private
def message
"[#{project_link}] Issue #{issue_link} #{state} by #{user_combined_name}"
end
def opened_issue?
action == 'open'
end
def description_message
[{
title: issue_title,
title_link: issue_url,
text: format(description),
color: '#C95823'
}]
end
def project_link
link(project_name, project_url)
end
def issue_link
link(issue_title, issue_url)
end
def issue_title
"#{Issue.reference_prefix}#{issue_iid} #{title}"
end
end
end
# frozen_string_literal: true
module ChatMessage
class MergeMessage < BaseMessage
attr_reader :merge_request_iid
attr_reader :source_branch
attr_reader :target_branch
attr_reader :action
attr_reader :state
attr_reader :title
def initialize(params)
super
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@merge_request_iid = obj_attr[:iid]
@source_branch = obj_attr[:source_branch]
@target_branch = obj_attr[:target_branch]
@action = obj_attr[:action]
@state = obj_attr[:state]
@title = format_title(obj_attr[:title])
end
def attachments
[]
end
def activity
{
title: "Merge request #{state_or_action_text} by #{user_combined_name}",
subtitle: "in #{project_link}",
text: merge_request_link,
image: user_avatar
}
end
private
def format_title(title)
'*' + title.lines.first.chomp + '*'
end
def message
merge_request_message
end
def project_link
link(project_name, project_url)
end
def merge_request_message
"#{user_combined_name} #{state_or_action_text} merge request #{merge_request_link} in #{project_link}"
end
def merge_request_link
link(merge_request_title, merge_request_url)
end
def merge_request_title
"#{MergeRequest.reference_prefix}#{merge_request_iid} #{title}"
end
def merge_request_url
"#{project_url}/-/merge_requests/#{merge_request_iid}"
end
def state_or_action_text
case action
when 'approved', 'unapproved'
action
when 'approval'
'added their approval to'
when 'unapproval'
'removed their approval from'
else
state
end
end
end
end
# frozen_string_literal: true
module ChatMessage
class NoteMessage < BaseMessage
attr_reader :note
attr_reader :note_url
attr_reader :title
attr_reader :target
def initialize(params)
super
params = HashWithIndifferentAccess.new(params)
obj_attr = params[:object_attributes]
@note = obj_attr[:note]
@note_url = obj_attr[:url]
@target, @title = case obj_attr[:noteable_type]
when "Commit"
create_commit_note(params[:commit])
when "Issue"
create_issue_note(params[:issue])
when "MergeRequest"
create_merge_note(params[:merge_request])
when "Snippet"
create_snippet_note(params[:snippet])
end
end
def attachments
return note if markdown
description_message
end
def activity
{
title: "#{user_combined_name} #{link('commented on ' + target, note_url)}",
subtitle: "in #{project_link}",
text: formatted_title,
image: user_avatar
}
end
private
def message
"#{user_combined_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{formatted_title}*"
end
def format_title(title)
title.lines.first.chomp
end
def formatted_title
format_title(title)
end
def create_issue_note(issue)
["issue #{Issue.reference_prefix}#{issue[:iid]}", issue[:title]]
end
def create_commit_note(commit)
commit_sha = Commit.truncate_sha(commit[:id])
["commit #{commit_sha}", commit[:message]]
end
def create_merge_note(merge_request)
["merge request #{MergeRequest.reference_prefix}#{merge_request[:iid]}", merge_request[:title]]
end
def create_snippet_note(snippet)
["snippet #{Snippet.reference_prefix}#{snippet[:id]}", snippet[:title]]
end
def description_message
[{ text: format(note), color: attachment_color }]
end
def project_link
link(project_name, project_url)
end
end
end
# frozen_string_literal: true
module ChatMessage
class PipelineMessage < BaseMessage
MAX_VISIBLE_JOBS = 10
attr_reader :user
attr_reader :ref_type
attr_reader :ref
attr_reader :status
attr_reader :detailed_status
attr_reader :duration
attr_reader :finished_at
attr_reader :pipeline_id
attr_reader :failed_stages
attr_reader :failed_jobs
attr_reader :project
attr_reader :commit
attr_reader :committer
attr_reader :pipeline
def initialize(data)
super
@user = data[:user]
@user_name = data.dig(:user, :username) || 'API'
pipeline_attributes = data[:object_attributes]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status]
@detailed_status = pipeline_attributes[:detailed_status]
@duration = pipeline_attributes[:duration].to_i
@finished_at = pipeline_attributes[:finished_at] ? Time.parse(pipeline_attributes[:finished_at]).to_i : nil
@pipeline_id = pipeline_attributes[:id]
# Get list of jobs that have actually failed (after exhausting all retries)
@failed_jobs = actually_failed_jobs(Array(data[:builds]))
@failed_stages = @failed_jobs.map { |j| j[:stage] }.uniq
@project = Project.find(data[:project][:id])
@commit = project.commit_by(oid: data[:commit][:id])
@committer = commit.committer
@pipeline = Ci::Pipeline.find(pipeline_id)
end
def pretext
''
end
def attachments
return message if markdown
[{
fallback: format(message),
color: attachment_color,
author_name: user_combined_name,
author_icon: user_avatar,
author_link: author_url,
title: s_("ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}") %
{
pipeline_id: pipeline_id,
humanized_status: humanized_status,
duration: pretty_duration(duration)
},
title_link: pipeline_url,
fields: attachments_fields,
footer: project.name,
footer_icon: project.avatar_url(only_path: false),
ts: finished_at
}]
end
def activity
{
title: s_("ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}") %
{
pipeline_link: pipeline_link,
ref_type: ref_type,
ref_link: ref_link,
user_combined_name: user_combined_name,
humanized_status: humanized_status
},
subtitle: s_("ChatMessage|in %{project_link}") % { project_link: project_link },
text: s_("ChatMessage|in %{duration}") % { duration: pretty_duration(duration) },
image: user_avatar || ''
}
end
private
def actually_failed_jobs(builds)
succeeded_job_names = builds.map { |b| b[:name] if b[:status] == 'success' }.compact.uniq
failed_jobs = builds.select do |build|
# Select jobs which doesn't have a successful retry
build[:status] == 'failed' && !succeeded_job_names.include?(build[:name])
end
failed_jobs.uniq { |job| job[:name] }.reverse
end
def failed_stages_field
{
title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
value: Slack::Messenger::Util::LinkFormatter.format(failed_stages_links),
short: true
}
end
def failed_jobs_field
{
title: s_("ChatMessage|Failed job").pluralize(failed_jobs.length),
value: Slack::Messenger::Util::LinkFormatter.format(failed_jobs_links),
short: true
}
end
def yaml_error_field
{
title: s_("ChatMessage|Invalid CI config YAML file"),
value: pipeline.yaml_errors,
short: false
}
end
def attachments_fields
fields = [
{
title: ref_type == "tag" ? s_("ChatMessage|Tag") : s_("ChatMessage|Branch"),
value: Slack::Messenger::Util::LinkFormatter.format(ref_link),
short: true
},
{
title: s_("ChatMessage|Commit"),
value: Slack::Messenger::Util::LinkFormatter.format(commit_link),
short: true
}
]
fields << failed_stages_field if failed_stages.any?
fields << failed_jobs_field if failed_jobs.any?
fields << yaml_error_field if pipeline.has_yaml_errors?
fields
end
def message
s_("ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}") %
{
project_link: project_link,
pipeline_link: pipeline_link,
ref_type: ref_type,
ref_link: ref_link,
user_combined_name: user_combined_name,
humanized_status: humanized_status,
duration: pretty_duration(duration)
}
end
def humanized_status
case status
when 'success'
detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed")
when 'failed'
s_("ChatMessage|has failed")
else
status
end
end
def attachment_color
case status
when 'success'
detailed_status == 'passed with warnings' ? 'warning' : 'good'
else
'danger'
end
end
def ref_url
if ref_type == 'tag'
"#{project_url}/-/tags/#{ref}"
else
"#{project_url}/-/commits/#{ref}"
end
end
def ref_link
"[#{ref}](#{ref_url})"
end
def project_url
project.web_url
end
def project_link
"[#{project.name}](#{project_url})"
end
def pipeline_failed_jobs_url
"#{project_url}/-/pipelines/#{pipeline_id}/failures"
end
def pipeline_url
if failed_jobs.any?
pipeline_failed_jobs_url
else
"#{project_url}/-/pipelines/#{pipeline_id}"
end
end
def pipeline_link
"[##{pipeline_id}](#{pipeline_url})"
end
def job_url(job)
"#{project_url}/-/jobs/#{job[:id]}"
end
def job_link(job)
"[#{job[:name]}](#{job_url(job)})"
end
def failed_jobs_links
failed = failed_jobs.slice(0, MAX_VISIBLE_JOBS)
truncated = failed_jobs.slice(MAX_VISIBLE_JOBS, failed_jobs.size)
failed_links = failed.map { |job| job_link(job) }
unless truncated.blank?
failed_links << s_("ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})") % {
count: truncated.size,
pipeline_failed_jobs_url: pipeline_failed_jobs_url
}
end
failed_links.join(I18n.translate(:'support.array.words_connector'))
end
def stage_link(stage)
# All stages link to the pipeline page
"[#{stage}](#{pipeline_url})"
end
def failed_stages_links
failed_stages.map { |s| stage_link(s) }.join(I18n.translate(:'support.array.words_connector'))
end
def commit_url
Gitlab::UrlBuilder.build(commit)
end
def commit_link
"[#{commit.title}](#{commit_url})"
end
def author_url
return unless user && committer
Gitlab::UrlBuilder.build(committer)
end
end
end
# frozen_string_literal: true
module ChatMessage
class PushMessage < BaseMessage
attr_reader :after
attr_reader :before
attr_reader :commits
attr_reader :ref
attr_reader :ref_type
def initialize(params)
super
@after = params[:after]
@before = params[:before]
@commits = params.fetch(:commits, [])
@ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
@ref = Gitlab::Git.ref_name(params[:ref])
end
def attachments
return [] if new_branch? || removed_branch?
return commit_messages if markdown
commit_message_attachments
end
def activity
{
title: humanized_action(short: true),
subtitle: "in #{project_link}",
text: compare_link,
image: user_avatar
}
end
private
def humanized_action(short: false)
action, ref_link, target_link = compose_action_details
text = [user_combined_name, action, ref_type, ref_link]
text << target_link unless short
text.join(' ')
end
def message
humanized_action
end
def format(string)
Slack::Messenger::Util::LinkFormatter.format(string)
end
def commit_messages
commits.map { |commit| compose_commit_message(commit) }.join("\n\n")
end
def commit_message_attachments
[{ text: format(commit_messages), color: attachment_color }]
end
def compose_commit_message(commit)
author = commit[:author][:name]
id = Commit.truncate_sha(commit[:id])
title = commit[:title]
url = commit[:url]
"[#{id}](#{url}): #{title} - #{author}"
end
def new_branch?
Gitlab::Git.blank_ref?(before)
end
def removed_branch?
Gitlab::Git.blank_ref?(after)
end
def ref_url
if ref_type == 'tag'
"#{project_url}/-/tags/#{ref}"
else
"#{project_url}/commits/#{ref}"
end
end
def compare_url
"#{project_url}/compare/#{before}...#{after}"
end
def ref_link
"[#{ref}](#{ref_url})"
end
def project_link
"[#{project_name}](#{project_url})"
end
def compare_link
"[Compare changes](#{compare_url})"
end
def compose_action_details
if new_branch?
['pushed new', ref_link, "to #{project_link}"]
elsif removed_branch?
['removed', ref, "from #{project_link}"]
else
['pushed to', ref_link, "of #{project_link} (#{compare_link})"]
end
end
def attachment_color
'#345'
end
end
end
# frozen_string_literal: true
module ChatMessage
class WikiPageMessage < BaseMessage
attr_reader :title
attr_reader :wiki_page_url
attr_reader :action
attr_reader :description
def initialize(params)
super
obj_attr = params[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
@title = obj_attr[:title]
@wiki_page_url = obj_attr[:url]
@description = obj_attr[:message]
@action =
case obj_attr[:action]
when "create"
"created"
when "update"
"edited"
end
end
def attachments
return description if markdown
description_message
end
def activity
{
title: "#{user_combined_name} #{action} #{wiki_page_link}",
subtitle: "in #{project_link}",
text: title,
image: user_avatar
}
end
private
def message
"#{user_combined_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
end
def description_message
[{ text: format(@description), color: attachment_color }]
end
def project_link
"[#{project_name}](#{project_url})"
end
def wiki_page_link
"[wiki page](#{wiki_page_url})"
end
end
end
...@@ -185,19 +185,19 @@ class ChatNotificationService < Integration ...@@ -185,19 +185,19 @@ class ChatNotificationService < Integration
def get_message(object_kind, data) def get_message(object_kind, data)
case object_kind case object_kind
when "push", "tag_push" when "push", "tag_push"
ChatMessage::PushMessage.new(data) if notify_for_ref?(data) Integrations::ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
when "issue" when "issue"
ChatMessage::IssueMessage.new(data) unless update?(data) Integrations::ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request" when "merge_request"
ChatMessage::MergeMessage.new(data) unless update?(data) Integrations::ChatMessage::MergeMessage.new(data) unless update?(data)
when "note" when "note"
ChatMessage::NoteMessage.new(data) Integrations::ChatMessage::NoteMessage.new(data)
when "pipeline" when "pipeline"
ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) Integrations::ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page" when "wiki_page"
ChatMessage::WikiPageMessage.new(data) Integrations::ChatMessage::WikiPageMessage.new(data)
when "deployment" when "deployment"
ChatMessage::DeploymentMessage.new(data) Integrations::ChatMessage::DeploymentMessage.new(data)
end end
end end
......
...@@ -39,7 +39,7 @@ class SlackService < ChatNotificationService ...@@ -39,7 +39,7 @@ class SlackService < ChatNotificationService
end end
def get_message(object_kind, data) def get_message(object_kind, data)
return ChatMessage::AlertMessage.new(data) if object_kind == 'alert' return Integrations::ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
super super
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ChatMessage::AlertMessage do RSpec.describe Integrations::ChatMessage::AlertMessage do
subject { described_class.new(args) } subject { described_class.new(args) }
let_it_be(:start_time) { Time.current } let_it_be(:start_time) { Time.current }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ChatMessage::BaseMessage do RSpec.describe Integrations::ChatMessage::BaseMessage do
let(:base_message) { described_class.new(args) } let(:base_message) { described_class.new(args) }
let(:args) { { project_url: 'https://gitlab-domain.com' } } let(:args) { { project_url: 'https://gitlab-domain.com' } }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ChatMessage::DeploymentMessage do RSpec.describe Integrations::ChatMessage::DeploymentMessage do
describe '#pretext' do describe '#pretext' do
it 'returns a message with the data returned by the deployment data builder' do it 'returns a message with the data returned by the deployment data builder' do
environment = create(:environment, name: "myenvironment") environment = create(:environment, name: "myenvironment")
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ChatMessage::IssueMessage do RSpec.describe Integrations::ChatMessage::IssueMessage do
subject { described_class.new(args) } subject { described_class.new(args) }
let(:args) do let(:args) do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ChatMessage::MergeMessage do RSpec.describe Integrations::ChatMessage::MergeMessage do
subject { described_class.new(args) } subject { described_class.new(args) }
let(:args) do let(:args) do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ChatMessage::NoteMessage do RSpec.describe Integrations::ChatMessage::NoteMessage do
subject { described_class.new(args) } subject { described_class.new(args) }
let(:color) { '#345' } let(:color) { '#345' }
......
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
RSpec.describe ChatMessage::PipelineMessage do RSpec.describe Integrations::ChatMessage::PipelineMessage do
subject { described_class.new(args) } subject { described_class.new(args) }
let(:args) do let(:args) do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ChatMessage::PushMessage do RSpec.describe Integrations::ChatMessage::PushMessage do
subject { described_class.new(args) } subject { described_class.new(args) }
let(:args) do let(:args) do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ChatMessage::WikiPageMessage do RSpec.describe Integrations::ChatMessage::WikiPageMessage do
subject { described_class.new(args) } subject { described_class.new(args) }
let(:args) do let(:args) do
......
...@@ -240,7 +240,7 @@ RSpec.describe MicrosoftTeamsService do ...@@ -240,7 +240,7 @@ RSpec.describe MicrosoftTeamsService do
chat_service.execute(data) chat_service.execute(data)
message = ChatMessage::PipelineMessage.new(data) message = Integrations::ChatMessage::PipelineMessage.new(data)
expect(WebMock).to have_requested(:post, webhook_url) expect(WebMock).to have_requested(:post, webhook_url)
.with(body: hash_including({ summary: message.summary })) .with(body: hash_including({ summary: message.summary }))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment