Commit 4637bfd6 authored by Z.J. van de Weg's avatar Z.J. van de Weg
Browse files

Create Slack Slash command service

parent 8af4d07ec5bd
......@@ -170,7 +170,7 @@
gem 'gemnasium-gitlab-service', '~> 0.2'
# Slack integration
gem 'slack-notifier', '~> 1.2.0'
gem 'slack-notifier', '~> 1.5.1'
# Asana integration
gem 'asana', '~> 0.4.0'
......
......@@ -687,7 +687,7 @@
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
slack-notifier (1.2.1)
slack-notifier (1.5.1)
slop (3.6.0)
spinach (0.8.10)
colorize
......@@ -957,7 +957,7 @@
sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0)
slack-notifier (~> 1.2.0)
slack-notifier (~> 1.5.1)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
spring (~> 1.7.0)
......
......@@ -96,6 +96,7 @@
has_one :gemnasium_service, dependent: :destroy
has_one :mattermost_slash_commands_service, dependent: :destroy
has_one :mattermost_notification_service, dependent: :destroy
has_one :slack_slash_commands_service, dependent: :destroy
has_one :slack_notification_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
......
class SlackSlashCommandsService < ChatService
include TriggersHelper
prop_accessor :token
def can_test?
false
end
def title
'Slack Slash Command'
end
def description
"Perform common operations on GitLab in Slack"
end
def to_param
'slack_slash_commands'
end
def fields
[
{ type: 'text', name: 'token', placeholder: '' }
]
end
def trigger(params)
return nil unless valid_token?(params[:token])
user = find_chat_user(params)
unless user
url = authorize_chat_name_url(params)
return Gitlab::ChatCommands::Presenters::Access.new(url).authorize
end
Gitlab::ChatCommands::Command.new(project, user, params).execute
end
private
def find_chat_user(params)
ChatNames::FindUserService.new(self, params).execute
end
def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute
end
end
......@@ -216,7 +216,8 @@
jira
kubernetes
mattermost_slash_commands
mattermost_notification
pipelines_email
pivotaltracker
pushover
redmine
......@@ -219,8 +220,8 @@
pipelines_email
pivotaltracker
pushover
redmine
mattermost_notification
slack_slash_commands
slack_notification
teamcity
]
......
- pretty_path_with_namespace = "#{@project ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}"
- pretty_path_with_namespace = "#{@project.namespace ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}"
- run_actions_text = "Perform common operations on this project: #{pretty_path_with_namespace}"
.well
......
---
title: Refactor presenters ChatCommands
merge_request: 7846
author:
......@@ -4,7 +4,7 @@
include Gitlab::Routing.url_helpers
def self.match(text)
/\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text)
/\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end
def self.help_message
......
module Gitlab
module ChatCommands
class Help < BaseCommand
# This class has to be used last, as it always matches. It has to match
# because other commands were not triggered and we want to show the help
# command
def self.match(_text)
true
end
def self.help_message
'help'
end
def self.allowed?(_project, _user)
true
end
def execute(commands)
Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger)
end
def trigger
params[:command]
end
end
end
end
module Mattermost
class Presenter
class << self
include Gitlab::Routing.url_helpers
def authorize_chat_name(url)
message = if url
":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})."
else
":sweat_smile: Couldn't identify you, nor can I autorize you!"
end
ephemeral_response(message)
end
def help(commands, trigger)
if commands.none?
ephemeral_response("No commands configured")
else
commands.map! { |command| "#{trigger} #{command}" }
message = header_with_list("Available commands", commands)
ephemeral_response(message)
end
end
def present(subject)
return not_found unless subject
if subject.is_a?(Gitlab::ChatCommands::Result)
show_result(subject)
elsif subject.respond_to?(:count)
if subject.many?
multiple_resources(subject)
elsif subject.none?
not_found
else
single_resource(subject)
end
else
single_resource(subject)
end
end
def access_denied
ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
end
private
def show_result(result)
case result.type
when :success
in_channel_response(result.message)
else
ephemeral_response(result.message)
end
end
def not_found
ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
end
def single_resource(resource)
return error(resource) if resource.errors.any? || !resource.persisted?
message = "### #{title(resource)}"
message << "\n\n#{resource.description}" if resource.try(:description)
in_channel_response(message)
end
def multiple_resources(resources)
resources.map! { |resource| title(resource) }
message = header_with_list("Multiple results were found:", resources)
ephemeral_response(message)
end
def error(resource)
message = header_with_list("The action was not successful, because:", resource.errors.messages)
ephemeral_response(message)
end
def title(resource)
reference = resource.try(:to_reference) || resource.try(:id)
title = resource.try(:title) || resource.try(:name)
"[#{reference} #{title}](#{url(resource)})"
end
def header_with_list(header, items)
message = [header]
items.each do |item|
message << "- #{item}"
end
message.join("\n")
end
def url(resource)
url_for(
[
resource.project.namespace.becomes(Namespace),
resource.project,
resource
]
)
end
def ephemeral_response(message)
{
response_type: :ephemeral,
text: message,
status: 200
}
end
def in_channel_response(message)
{
response_type: :in_channel,
text: message,
status: 200
}
end
end
end
end
module Mattermost
class Presenter
class << self
include Gitlab::Routing.url_helpers
def authorize_chat_name(url)
message = if url
":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})."
else
":sweat_smile: Couldn't identify you, nor can I autorize you!"
end
ephemeral_response(message)
end
def help(commands, trigger)
if commands.none?
ephemeral_response("No commands configured")
else
commands.map! { |command| "#{trigger} #{command}" }
message = header_with_list("Available commands", commands)
ephemeral_response(message)
end
end
def present(subject)
return not_found unless subject
if subject.is_a?(Gitlab::ChatCommands::Result)
show_result(subject)
elsif subject.respond_to?(:count)
if subject.many?
multiple_resources(subject)
elsif subject.none?
not_found
else
single_resource(subject)
end
else
single_resource(subject)
end
end
def access_denied
ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
end
private
def show_result(result)
case result.type
when :success
in_channel_response(result.message)
else
ephemeral_response(result.message)
end
end
def not_found
ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
end
def single_resource(resource)
return error(resource) if resource.errors.any? || !resource.persisted?
message = "### #{title(resource)}"
message << "\n\n#{resource.description}" if resource.try(:description)
in_channel_response(message)
end
def multiple_resources(resources)
resources.map! { |resource| title(resource) }
message = header_with_list("Multiple results were found:", resources)
ephemeral_response(message)
end
def error(resource)
message = header_with_list("The action was not successful, because:", resource.errors.messages)
ephemeral_response(message)
end
def title(resource)
reference = resource.try(:to_reference) || resource.try(:id)
title = resource.try(:title) || resource.try(:name)
"[#{reference} #{title}](#{url(resource)})"
end
def header_with_list(header, items)
message = [header]
items.each do |item|
message << "- #{item}"
end
message.join("\n")
end
def url(resource)
url_for(
[
resource.project.namespace.becomes(Namespace),
resource.project,
resource
]
)
end
def ephemeral_response(message)
{
response_type: :ephemeral,
text: message,
status: 200
}
end
def in_channel_response(message)
{
response_type: :in_channel,
text: message,
status: 200
}
end
end
end
end
......@@ -64,7 +64,7 @@
context 'and user can not create deployment' do
it 'returns action' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to start_with('Whoops! That action is not allowed')
expect(subject[:text]).to start_with('Whoops! This action is not allowed')
end
end
......@@ -74,7 +74,7 @@
end
it 'returns action' do
expect(subject[:text]).to include('Deployment from staging to production started')
expect(subject[:text]).to include('Deployment started from staging to production')
expect(subject[:response_type]).to be(:in_channel)
end
......@@ -91,4 +91,26 @@
end
end
end
describe '#match_command' do
subject { described_class.new(project, user, params).match_command.first }
context 'IssueShow is triggered' do
let(:params) { { text: 'issue show 123' } }
it { is_expected.to eq(Gitlab::ChatCommands::IssueShow) }
end
context 'IssueCreate is triggered' do
let(:params) { { text: 'issue create my title' } }
it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) }
end
context 'IssueSearch is triggered' do
let(:params) { { text: 'issue search my query' } }
it { is_expected.to eq(Gitlab::ChatCommands::IssueSearch) }
end
end
end
......@@ -10,7 +10,7 @@
tag: false,
project_name: 'project_name',
project_url: 'example.gitlab.com',
project_url: 'http://example.gitlab.com',
commit: {
status: status,
......@@ -48,6 +48,6 @@
end
def build_message(status_text = status)
"<example.gitlab.com|project_name>:" \
" Commit <example.gitlab.com/commit/" \
"<http://example.gitlab.com|project_name>:" \
" Commit <http://example.gitlab.com/commit/" \
"97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
......@@ -53,5 +53,5 @@
"97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
" of <example.gitlab.com/commits/develop|develop> branch" \
" of <http://example.gitlab.com/commits/develop|develop> branch" \
" by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end
end
......@@ -10,10 +10,10 @@
username: 'test.user'
},
project_name: 'project_name',
project_url: 'somewhere.com',
project_url: 'http://somewhere.com',
object_attributes: {
title: 'Issue title',
id: 10,
iid: 100,
assignee_id: 1,
......@@ -14,10 +14,10 @@
object_attributes: {
title: 'Issue title',
id: 10,
iid: 100,
assignee_id: 1,
url: 'url',
url: 'http://url.com',
action: 'open',
state: 'opened',
description: 'issue description'
......@@ -40,7 +40,7 @@
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
'<somewhere.com|[project_name>] Issue opened by test.user')
'[<http://somewhere.com|project_name>] Issue opened by test.user')
expect(subject.attachments).to eq([
{
title: "#100 Issue title",
......@@ -44,7 +44,7 @@
expect(subject.attachments).to eq([
{
title: "#100 Issue title",
title_link: "url",
title_link: "http://url.com",
text: "issue description",
color: color,
}
......@@ -60,7 +60,7 @@
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
'<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user')
'[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> closed by test.user')
expect(subject.attachments).to be_empty
end
end
......
......@@ -10,10 +10,10 @@
username: 'test.user'
},
project_name: 'project_name',
project_url: 'somewhere.com',
project_url: 'http://somewhere.com',
object_attributes: {
title: "Issue title\nSecond line",
id: 10,
iid: 100,
assignee_id: 1,
......@@ -14,10 +14,10 @@
object_attributes: {
title: "Issue title\nSecond line",
id: 10,
iid: 100,
assignee_id: 1,
url: 'url',
url: 'http://url.com',
state: 'opened',
description: 'issue description',
source_branch: 'source_branch',
......@@ -31,8 +31,8 @@
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*')
'test.user opened <http://somewhere.com/merge_requests/100|merge request !100> '\
'in <http://somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
end
......@@ -43,8 +43,8 @@
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*')
'test.user closed <http://somewhere.com/merge_requests/100|merge request !100> '\
'in <http://somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
end
......
......@@ -11,6 +11,6 @@
avatar_url: 'http://fakeavatar'
},
project_name: 'project_name',
project_url: 'somewhere.com',
project_url: 'http://somewhere.com',
repository: {
name: 'project_name',
......@@ -15,7 +15,7 @@
repository: {
name: 'project_name',
url: 'somewhere.com',
url: 'http://somewhere.com',
},
object_attributes: {
id: 10,
note: 'comment on a commit',
......@@ -18,8 +18,8 @@
},
object_attributes: {
id: 10,
note: 'comment on a commit',
url: 'url',
url: 'http://url.com',
noteable_type: 'Commit'