diff options
author | Z.J. van de Weg <zegerjan@gitlab.com> | 2016-08-08 08:24:18 +0200 |
---|---|---|
committer | Z.J. van de Weg <zegerjan@gitlab.com> | 2016-08-08 13:34:39 +0200 |
commit | b505a6525ae690c49806b47524caea34aa36a44c (patch) | |
tree | 446eb2085b4f3891ed1f247ed7d9c12d63816724 | |
parent | 154f36346e5f8626f4328d2251a131a46146c4ec (diff) | |
download | gitlab-ce-zj-mattermost-service.tar.gz |
Start work on a Mattermost servicezj-mattermost-service
15 files changed, 904 insertions, 0 deletions
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb new file mode 100644 index 00000000000..ad415fa98a8 --- /dev/null +++ b/app/models/project_services/mattermost_service.rb @@ -0,0 +1,130 @@ +class MattermostService < Service + boolean_accessor :notify_only_broken_builds + prop_accessor :webhook, :username, :channel + + validates :webhook, presence: true, url: true, if: :activated? + + def initialize_properties + # Custom serialized properties initialization + self.supported_events.each { |event| self.class.prop_accessor(event_channel_name(event)) } + + if properties.nil? + self.properties = {} + self.notify_only_broken_builds = true + end + end + + def title + 'Mattermost' + end + + def description + 'Self-hosted Slack-alternative' + end + + def help + 'This service sends notifications to your Mattermost instance.<br/> + To setup this Service you need to create a new <b>"Incoming webhook"</b> on the Mattermost integrations panel, + and enter the Webhook URL below. You can override the Channels if needed.' + end + + def to_param + 'mattermost' + end + + def fields + default_fields = + [ + { type: 'text', name: 'webhook', placeholder: 'http://mattermost.company.com/hooks/...' }, + { type: 'text', name: 'username', placeholder: 'GitLab' }, + { type: 'text', name: 'channel', placeholder: "town-square" }, + { type: 'checkbox', name: 'notify_only_broken_builds' }, + ] + + default_fields + build_event_channels + end + + def supported_events + %w(push issue merge_request note tag_push build wiki_page) + end + + def execute(data) + return unless webhook.present? + + data = data.with_indifferent_access + return unless supported_events.include?(data[:object_kind]) + + message = message(data) + return unless message + + message.send(webhook, channel: channel, username: username || 'GitLab') + 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 + + private + + def get_channel_field(event) + field_name = event_channel_name(event) + self.public_send(field_name) + end + + def build_event_channels + supported_events.reduce([]) do |channels, event| + channels << { type: 'text', name: event_channel_name(event), placeholder: "town-square" } + end + end + + def event_channel_name(event) + "#{event}_channel" + end + + def message(data) + case data[:object_kind] + when "push", "tag_push" + PushMessage.new(data) + when "issue" + IssueMessage.new(data) unless is_update?(data) + when "merge_request" + MergeMessage.new(data) unless is_update?(data) + when "note" + NoteMessage.new(data) + when "build" + BuildMessage.new(data) if should_build_be_notified?(data) + when "wiki_page" + WikiPageMessage.new(data) + end + end + + def is_update?(data) + data[:object_attributes][:action] == 'update' + end + + def should_build_be_notified?(data) + case data[:commit][:status] + when 'success' + !notify_only_broken_builds? + when 'failed' + true + else + false + end + end +end + +require "mattermost_service/issue_message" +require "mattermost_service/push_message" +require "mattermost_service/merge_message" +require "mattermost_service/note_message" +require "mattermost_service/build_message" +require "mattermost_service/wiki_page_message" diff --git a/app/models/project_services/mattermost_service/base_message.rb b/app/models/project_services/mattermost_service/base_message.rb new file mode 100644 index 00000000000..23461f23f50 --- /dev/null +++ b/app/models/project_services/mattermost_service/base_message.rb @@ -0,0 +1,72 @@ +class MattermostService + class BaseMessage + def initialize(params) + raise NotImplementedError + end + + def send(webhook, opts) + body = opts.merge(text: message) + HTTParty.post(webhook, headers: { 'Content-Type' => 'application/json' }, + body: body.to_json) + end + + def message + "**#{header}**\n\n" + + "#{body}\n\n" + + footer + end + + private + + def header + raise NotImplementedError + end + + def body + body = "> ### #{title}\n" + + body << "> #{description.truncate(400)}\n" if description + body << "\n" + end + + def footer + "[View on GitLab](#{resource_url})" + end + + # Override this method unless your resource has a title field + def title + params[:object_attributes][:title] + end + + # Implemention in sub class not required + def description + params[:object_attributes][:description] + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def project_name + params[:project][:path_with_namespace].gsub(/\s/, '') + end + + def project_url + params[:project][:web_url] + end + + def resource_url + params[:object_attributes][:url] + end + + def resource_id + params[:object_attributes][:iid] || params[:object_attributes][:id] + end + + def user_name + params[:user][:name] + end + + def + end +end diff --git a/app/models/project_services/mattermost_service/build_message.rb b/app/models/project_services/mattermost_service/build_message.rb new file mode 100644 index 00000000000..053ec46f680 --- /dev/null +++ b/app/models/project_services/mattermost_service/build_message.rb @@ -0,0 +1,60 @@ +class MattermostService + class BuildMessage < BaseMessage + attr_reader :params + + def initialize(params) + @params = params + + puts '-' * 40 + puts params + puts '-' * 40 + end + + private + + def header + "Build #{state} on #{ref} by #{user_name} at #{project_link}" + end + + def title + "[#{ref}](#{ref_link}) #{failed} in #{duration} #{'second'.pluralize(duration)}" + end + + def description + if failed? + "During #{stage} build #{build_name} " + else + + end + end + + def humanized_status + case status + when 'success' + 'passed' + else + status + end + end + + def branch_url + "#{project_url}/commits/#{ref}" + end + + def branch_link + "[#{ref}](#{branch_url})" + end + + def project_link + "[#{project_name}](#{project_url})" + end + + def commit_url + "#{project_url}/commit/#{sha}/builds" + end + + def commit_link + "[#{Commit.truncate_sha(sha)}](#{commit_url})" + end + end +end diff --git a/app/models/project_services/mattermost_service/issue_message.rb b/app/models/project_services/mattermost_service/issue_message.rb new file mode 100644 index 00000000000..3e917565eca --- /dev/null +++ b/app/models/project_services/mattermost_service/issue_message.rb @@ -0,0 +1,27 @@ +class MattermostService + class IssueMessage < BaseMessage + attr_reader :params + + def initialize(params) + @params = params + end + + private + + def header + "#{user_name} #{action} #{issue_link} on #{project_link}" + end + + def action + if params[:object_attributes][:action] == 'open' + 'opened' + else + 'closed' + end + end + + def issue_link + "[issue ##{resource_id}](#{resource_url})" + end + end +end diff --git a/app/models/project_services/mattermost_service/merge_message.rb b/app/models/project_services/mattermost_service/merge_message.rb new file mode 100644 index 00000000000..eade3967fbe --- /dev/null +++ b/app/models/project_services/mattermost_service/merge_message.rb @@ -0,0 +1,23 @@ +class MattermostService + class MergeMessage < BaseMessage + attr_reader :params + + def initialize(params) + @params = params + end + + private + + def header + "#{user_name} #{state} #{merge_request_link} on #{project_link}" + end + + def state + params[:object_attributes][:state] + end + + def merge_request_link + "[merge request !#{resource_id}](#{resource_url})" + end + end +end diff --git a/app/models/project_services/mattermost_service/note_message.rb b/app/models/project_services/mattermost_service/note_message.rb new file mode 100644 index 00000000000..304974b1645 --- /dev/null +++ b/app/models/project_services/mattermost_service/note_message.rb @@ -0,0 +1,38 @@ +class MattermostService + class NoteMessage < BaseMessage + attr_reader :params + + def initialize(params) + @params = params + end + + private + + def header + "#{user_name} left a note on #{resource_link} at #{project_link}" + end + + def title + params[:object_attributes][:note] + end + + def resource_link + "[#{noteable_type} #{resource_id}](resource_url)" + end + + def resource_id + noteable = params[noteable_type] + + # Support for Issue/MR/Commit/Snippet + if noteable == 'commit' + Commit.truncate_sha(noteable[:id]) + else + noteable[:iid] || noteable[:id] + end + end + + def noteable_type + params[:object_attributes][:noteable_type].to_s.underscore + end + end +end diff --git a/app/models/project_services/mattermost_service/push_message.rb b/app/models/project_services/mattermost_service/push_message.rb new file mode 100644 index 00000000000..93bb89ec7ae --- /dev/null +++ b/app/models/project_services/mattermost_service/push_message.rb @@ -0,0 +1,73 @@ +class MattermostService + class PushMessage < BaseMessage + attr_reader :params + + def initialize(params) + @params = params + end + + private + + def header + "#{user_name} #{action_text} #{project_link}" + end + + def action_text + if new_branch? + "pushed new #{ref_type} #{branch_link} to" + elsif removed_branch? + "removed #{ref_type} #{ref} from" + else + "pushed to #{ref_type} #{branch_link} of" + end + end + + def title + "Reviewer please help me here" + end + + def description + unless commits.empty? + commits.first(5).map do |commit| + "[#{Commit.truncate_sha(commit[:id])}](#{commit[:url]}) #{commit[:message].truncate(50)}" + end.join("\n") + end + end + + def ref_type + Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch' + end + + def ref + Gitlab::Git.ref_name(params[:ref]) + end + + def new_branch? + Gitlab::Git.blank_ref?(before) + end + + def removed_branch? + Gitlab::Git.blank_ref?(after) + end + + def before + params[:before] + end + + def after + params[:after] + end + + def branch_link + "[#{ref}](#{resource_url})" + end + + def resource_url + "#{project_url}/commits/#{ref}" + end + + def user_name + params[:user_name] + end + end +end diff --git a/app/models/project_services/mattermost_service/wiki_page_message.rb b/app/models/project_services/mattermost_service/wiki_page_message.rb new file mode 100644 index 00000000000..0e89a796ee2 --- /dev/null +++ b/app/models/project_services/mattermost_service/wiki_page_message.rb @@ -0,0 +1,32 @@ +class MattermostService + class WikiPageMessage < BaseMessage + attr_reader :params + + def initialize(params) + @params = params + end + + private + + def header + "#{user_name} #{action} a #{wiki_page_link} on #{project_link}" + end + + def description + params[:object_attributes][:content] + end + + def action + case params[:object_attributes][:action] + when "create" + "created" + when "update" + "edited" + end + end + + def wiki_page_link + "[wiki page](#{wiki_page_url})" + end + end +end diff --git a/app/models/service.rb b/app/models/service.rb index c55d3c7c75a..112a2f27673 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -203,6 +203,7 @@ class Service < ActiveRecord::Base hipchat irker jira + mattermost pivotaltracker pushover redmine diff --git a/spec/models/project_services/mattermost_service/build_message_spec.rb b/spec/models/project_services/mattermost_service/build_message_spec.rb new file mode 100644 index 00000000000..7fcfdf0eacd --- /dev/null +++ b/spec/models/project_services/mattermost_service/build_message_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe SlackService::BuildMessage do + subject { SlackService::BuildMessage.new(args) } + + let(:args) do + { + sha: '97de212e80737a608d939f648d959671fb0a0142', + ref: 'develop', + tag: false, + + project_name: 'project_name', + project_url: 'somewhere.com', + + commit: { + status: status, + author_name: 'hacker', + duration: duration, + }, + } + end + + context 'succeeded' do + let(:status) { 'success' } + let(:color) { 'good' } + let(:duration) { 10 } + + it 'returns a message with information about succeeded build' do + message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 seconds' + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + context 'failed' do + let(:status) { 'failed' } + let(:color) { 'danger' } + let(:duration) { 10 } + + it 'returns a message with information about failed build' do + message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 seconds' + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end + + describe '#seconds_name' do + let(:status) { 'failed' } + let(:color) { 'danger' } + let(:duration) { 1 } + + it 'returns seconds as singular when there is only one' do + message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 1 second' + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) + end + end +end diff --git a/spec/models/project_services/mattermost_service/issue_message_spec.rb b/spec/models/project_services/mattermost_service/issue_message_spec.rb new file mode 100644 index 00000000000..5fdbc4a697b --- /dev/null +++ b/spec/models/project_services/mattermost_service/issue_message_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe MattermostService::IssueMessage, models: true do + subject { MattermostService::IssueMessage.new(params) } + + let(:params) do + { + user: { + name: 'Test User', + username: 'Test User' + }, + project: { + path_with_namespace: 'root/test-project', + web_url: 'http://localhost:3000/root/empty' + }, + object_attributes: { + title: 'Issue title', + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + action: 'open', + state: 'opened', + description: 'issue description', + url: 'http://localhost:3000/root/empty/issues/7' + } + } + end + + context 'open' do + it 'returns a message regarding opening of issues' do + message = subject.message + + expect(message).to start_with '**Test User opened [' + expect(message).to match /View on GitLab/ + end + end + + context 'close' do + before do + params[:object_attributes][:action] = 'close' + params[:object_attributes][:state] = 'closed' + end + + it 'returns a message regarding closing of issues' do + expect(subject.message).to start_with "**Test User closed [" + end + end +end diff --git a/spec/models/project_services/mattermost_service/merge_message_spec.rb b/spec/models/project_services/mattermost_service/merge_message_spec.rb new file mode 100644 index 00000000000..e0bfba254f7 --- /dev/null +++ b/spec/models/project_services/mattermost_service/merge_message_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe MattermostService::MergeMessage, models: true do + subject { MattermostService::MergeMessage.new(params) } + + let(:params) do + { + user: { + name: 'Test User', + username: 'Test User' + }, + project: { + path_with_namespace: 'root/test-project', + web_url: 'http://localhost:3000/root/empty' + }, + object_attributes: { + title: "Issue title\nSecond line", + id: 10, + iid: 100, + assignee_id: 1, + url: 'url', + state: 'opened', + description: 'issue description', + source_branch: 'source_branch', + target_branch: 'target_branch', + } + } + end + + context 'opening a MR' do + it 'returns a message regarding opening of issues' do + message = subject.message + + expect(message).to start_with '**Test User opened [' + expect(message).to match /View on GitLab/ + end + end + + context 'merging a MR' do + before do + params[:object_attributes][:state] = 'closed' + end + + it 'returns a message regarding closing of issues' do + expect(subject.message).to start_with "**Test User closed [" + end + end +end diff --git a/spec/models/project_services/mattermost_service/note_message_spec.rb b/spec/models/project_services/mattermost_service/note_message_spec.rb new file mode 100644 index 00000000000..379c3e1219c --- /dev/null +++ b/spec/models/project_services/mattermost_service/note_message_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe SlackService::NoteMessage, models: true do + let(:color) { '#345' } + + before do + @args = { + user: { + name: 'Test User', + username: 'username', + avatar_url: 'http://fakeavatar' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + repository: { + name: 'project_name', + url: 'somewhere.com', + }, + object_attributes: { + id: 10, + note: 'comment on a commit', + url: 'url', + noteable_type: 'Commit' + } + } + end + + context 'commit notes' do + before do + @args[:object_attributes][:note] = 'comment on a commit' + @args[:object_attributes][:noteable_type] = 'Commit' + @args[:commit] = { + id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23', + message: "Added a commit message\ndetails\n123\n" + } + end + + it 'returns a message regarding notes on commits' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("Test User commented on " \ + "<url|commit 5f163b2b> in <somewhere.com|project_name>: " \ + "*Added a commit message*") + expected_attachments = [ + { + text: "comment on a commit", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'merge request notes' do + before do + @args[:object_attributes][:note] = 'comment on a merge request' + @args[:object_attributes][:noteable_type] = 'MergeRequest' + @args[:merge_request] = { + id: 1, + iid: 30, + title: "merge request title\ndetails\n" + } + end + it 'returns a message regarding notes on a merge request' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("Test User commented on " \ + "<url|merge request !30> in <somewhere.com|project_name>: " \ + "*merge request title*") + expected_attachments = [ + { + text: "comment on a merge request", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'issue notes' do + before do + @args[:object_attributes][:note] = 'comment on an issue' + @args[:object_attributes][:noteable_type] = 'Issue' + @args[:issue] = { + id: 1, + iid: 20, + title: "issue title\ndetails\n" + } + end + + it 'returns a message regarding notes on an issue' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq( + "Test User commented on " \ + "<url|issue #20> in <somewhere.com|project_name>: " \ + "*issue title*") + expected_attachments = [ + { + text: "comment on an issue", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end + + context 'project snippet notes' do + before do + @args[:object_attributes][:note] = 'comment on a snippet' + @args[:object_attributes][:noteable_type] = 'Snippet' + @args[:snippet] = { + id: 5, + title: "snippet title\ndetails\n" + } + end + + it 'returns a message regarding notes on a project snippet' do + message = SlackService::NoteMessage.new(@args) + expect(message.pretext).to eq("Test User commented on " \ + "<url|snippet #5> in <somewhere.com|project_name>: " \ + "*snippet title*") + expected_attachments = [ + { + text: "comment on a snippet", + color: color, + } + ] + expect(message.attachments).to eq(expected_attachments) + end + end +end diff --git a/spec/models/project_services/mattermost_service/push_message_spec.rb b/spec/models/project_services/mattermost_service/push_message_spec.rb new file mode 100644 index 00000000000..cda9ee670b0 --- /dev/null +++ b/spec/models/project_services/mattermost_service/push_message_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe SlackService::PushMessage, models: true do + subject { SlackService::PushMessage.new(args) } + + let(:args) do + { + after: 'after', + before: 'before', + project_name: 'project_name', + ref: 'refs/heads/master', + user_name: 'user_name', + project_url: 'url' + } + end + + let(:color) { '#345' } + + context 'push' do + before do + args[:commits] = [ + { message: 'message1', url: 'url1', id: 'abcdefghijkl', author: { name: 'author1' } }, + { message: 'message2', url: 'url2', id: '123456789012', author: { name: 'author2' } }, + ] + end + + it 'returns a message regarding pushes' do + expect(subject.pretext).to eq( + 'user_name pushed to branch <url/commits/master|master> of '\ + '<url|project_name> (<url/compare/before...after|Compare changes>)' + ) + expect(subject.attachments).to eq([ + { + text: "<url1|abcdefgh>: message1 - author1\n"\ + "<url2|12345678>: message2 - author2", + color: color, + } + ]) + end + end + + context 'tag push' do + let(:args) do + { + after: 'after', + before: Gitlab::Git::BLANK_SHA, + project_name: 'project_name', + ref: 'refs/tags/new_tag', + user_name: 'user_name', + project_url: 'url' + } + end + + it 'returns a message regarding pushes' do + expect(subject.pretext).to eq('user_name pushed new tag ' \ + '<url/commits/new_tag|new_tag> to ' \ + '<url|project_name>') + expect(subject.attachments).to be_empty + end + end + + context 'new branch' do + before do + args[:before] = Gitlab::Git::BLANK_SHA + end + + it 'returns a message regarding a new branch' do + expect(subject.pretext).to eq( + 'user_name pushed new branch <url/commits/master|master> to '\ + '<url|project_name>' + ) + expect(subject.attachments).to be_empty + end + end + + context 'removed branch' do + before do + args[:after] = Gitlab::Git::BLANK_SHA + end + + it 'returns a message regarding a removed branch' do + expect(subject.pretext).to eq( + 'user_name removed branch master from <url|project_name>' + ) + expect(subject.attachments).to be_empty + end + end +end diff --git a/spec/models/project_services/mattermost_service/wiki_page_message_spec.rb b/spec/models/project_services/mattermost_service/wiki_page_message_spec.rb new file mode 100644 index 00000000000..46dedb66c7c --- /dev/null +++ b/spec/models/project_services/mattermost_service/wiki_page_message_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe SlackService::WikiPageMessage, models: true do + subject { described_class.new(args) } + + let(:args) do + { + user: { + name: 'Test User', + username: 'Test User' + }, + project_name: 'project_name', + project_url: 'somewhere.com', + object_attributes: { + title: 'Wiki page title', + url: 'url', + content: 'Wiki page description' + } + } + end + + describe '#pretext' do + context 'when :action == "create"' do + before { args[:object_attributes][:action] = 'create' } + + it 'returns a message that a new wiki page was created' do + expect(subject.pretext).to eq( + 'Test User created <url|wiki page> in <somewhere.com|project_name>: '\ + '*Wiki page title*') + end + end + + context 'when :action == "update"' do + before { args[:object_attributes][:action] = 'update' } + + it 'returns a message that a wiki page was updated' do + expect(subject.pretext).to eq( + 'Test User edited <url|wiki page> in <somewhere.com|project_name>: '\ + '*Wiki page title*') + end + end + end + + describe '#attachments' do + let(:color) { '#345' } + + context 'when :action == "create"' do + before { args[:object_attributes][:action] = 'create' } + + it 'it returns the attachment for a new wiki page' do + expect(subject.attachments).to eq([ + { + text: "Wiki page description", + color: color, + } + ]) + end + end + + context 'when :action == "update"' do + before { args[:object_attributes][:action] = 'update' } + + it 'it returns the attachment for an updated wiki page' do + expect(subject.attachments).to eq([ + { + text: "Wiki page description", + color: color, + } + ]) + end + end + end +end |