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

Chat Commands have presenters

This improves the styling and readability of the code. This is supported
by both Mattermost and Slack.
parent b525aff6
...@@ -28,20 +28,24 @@ def fields ...@@ -28,20 +28,24 @@ def fields
end end
def trigger(params) def trigger(params)
return unless valid_token?(params[:token]) return access_presenter unless valid_token?(params[:token])
user = find_chat_user(params) user = find_chat_user(params)
unless user
if user
Gitlab::ChatCommands::Command.new(project, user, params).execute
else
url = authorize_chat_name_url(params) url = authorize_chat_name_url(params)
return presenter.authorize_chat_name(url) access_presenter(url).authorize
end end
Gitlab::ChatCommands::Command.new(project, user,
params).execute
end end
private private
def access_presenter(url = nil)
Gitlab::ChatCommands::Presenters::Access.new(url)
end
def find_chat_user(params) def find_chat_user(params)
ChatNames::FindUserService.new(self, params).execute ChatNames::FindUserService.new(self, params).execute
end end
...@@ -49,8 +53,4 @@ def find_chat_user(params) ...@@ -49,8 +53,4 @@ def find_chat_user(params)
def authorize_chat_name_url(params) def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute ChatNames::AuthorizeUserService.new(self, params).execute
end end
def presenter
Gitlab::ChatCommands::Presenter.new
end
end end
...@@ -42,10 +42,6 @@ def initialize(project, user, params = {}) ...@@ -42,10 +42,6 @@ def initialize(project, user, params = {})
def find_by_iid(iid) def find_by_iid(iid)
collection.find_by(iid: iid) collection.find_by(iid: iid)
end end
def presenter
Gitlab::ChatCommands::Presenter.new
end
end end
end end
end end
...@@ -13,9 +13,9 @@ def execute ...@@ -13,9 +13,9 @@ def execute
if command if command
if command.allowed?(project, current_user) if command.allowed?(project, current_user)
present command.new(project, current_user, params).execute(match) command.new(project, current_user, params).execute(match)
else else
access_denied Gitlab::ChatCommands::Presenters::Access.new.access_denied
end end
else else
help(help_messages) help(help_messages)
...@@ -25,7 +25,7 @@ def execute ...@@ -25,7 +25,7 @@ def execute
def match_command def match_command
match = nil match = nil
service = available_commands.find do |klass| service = available_commands.find do |klass|
match = klass.match(command) match = klass.match(params[:text])
end end
[service, match] [service, match]
...@@ -42,22 +42,6 @@ def available_commands ...@@ -42,22 +42,6 @@ def available_commands
klass.available?(project) klass.available?(project)
end end
end end
def command
params[:text]
end
def help(messages)
presenter.help(messages, params[:command])
end
def access_denied
presenter.access_denied
end
def present(resource)
presenter.present(resource)
end
end end
end end
end end
module Gitlab module Gitlab
module ChatCommands module ChatCommands
class Deploy < BaseCommand class Deploy < BaseCommand
include Gitlab::Routing.url_helpers
def self.match(text) def self.match(text)
/\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text) /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end end
...@@ -24,35 +22,29 @@ def execute(match) ...@@ -24,35 +22,29 @@ def execute(match)
to = match[:to] to = match[:to]
actions = find_actions(from, to) actions = find_actions(from, to)
return unless actions.present?
if actions.one? if actions.none?
play!(from, to, actions.first) Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions
elsif actions.one?
action = play!(from, to, actions.first)
Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to)
else else
Result.new(:error, 'Too many actions defined') Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions
end end
end end
private private
def play!(from, to, action) def play!(from, to, action)
new_action = action.play(current_user) action.play(current_user)
Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.")
end end
def find_actions(from, to) def find_actions(from, to)
environment = project.environments.find_by(name: from) environment = project.environments.find_by(name: from)
return unless environment return [] unless environment
environment.actions_for(to).select(&:starts_environment?) environment.actions_for(to).select(&:starts_environment?)
end end
def url(subject)
polymorphic_url(
[subject.project.namespace.becomes(Namespace), subject.project, subject]
)
end
end end
end end
end end
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
module ChatCommands module ChatCommands
class IssueCreate < IssueCommand class IssueCreate < IssueCommand
def self.match(text) def self.match(text)
# we can not match \n with the dot by passing the m modifier as than # we can not match \n with the dot by passing the m modifier as than
# the title and description are not seperated # the title and description are not seperated
/\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
end end
...@@ -19,8 +19,24 @@ def execute(match) ...@@ -19,8 +19,24 @@ def execute(match)
title = match[:title] title = match[:title]
description = match[:description].to_s.rstrip description = match[:description].to_s.rstrip
issue = create_issue(title: title, description: description)
if issue.errors.any?
presenter(issue).display_errors
else
presenter(issue).present
end
end
private
def create_issue(title:, description:)
Issues::CreateService.new(project, current_user, title: title, description: description).execute Issues::CreateService.new(project, current_user, title: title, description: description).execute
end end
def presenter(issue)
Gitlab::ChatCommands::Presenters::ShowIssue.new(issue)
end
end end
end end
end end
...@@ -10,7 +10,15 @@ def self.help_message ...@@ -10,7 +10,15 @@ def self.help_message
end end
def execute(match) def execute(match)
collection.search(match[:query]).limit(QUERY_LIMIT) issues = collection.search(match[:query]).limit(QUERY_LIMIT)
if issues.none?
Presenters::Access.new(issues).not_found
elsif issues.one?
Presenters::ShowIssue.new(issues.first).present
else
Presenters::ListIssues.new(issues).present
end
end end
end end
end end
......
...@@ -10,7 +10,13 @@ def self.help_message ...@@ -10,7 +10,13 @@ def self.help_message
end end
def execute(match) def execute(match)
find_by_iid(match[:iid]) issue = find_by_iid(match[:iid])
if issue
Gitlab::ChatCommands::Presenters::ShowIssue.new(issue).present
else
Gitlab::ChatCommands::Presenters::Access.new.not_found
end
end end
end end
end end
......
module Gitlab
module ChatCommands
class Presenter
include Gitlab::Routing
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.none?
not_found
elsif subject.one?
single_resource(subject.first)
else
multiple_resources(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)
titles = resources.map { |resource| title(resource) }
message = header_with_list("Multiple results were found:", titles)
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 Gitlab::ChatCommands::Presenters
class Access < Gitlab::ChatCommands::Presenters::Base
def access_denied
ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
end
def not_found
ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:")
end
def authorize
message =
if @resource
":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})."
else
":sweat_smile: Couldn't identify you, nor can I autorize you!"
end
ephemeral_response(text: message)
end
end
end
module Gitlab::ChatCommands::Presenters
class Base
include Gitlab::Routing.url_helpers
def initialize(resource = nil)
@resource = resource
end
def display_errors
message = header_with_list("The action was not successful, because:", @resource.errors.full_messages)
ephemeral_response(text: message)
end
private
def header_with_list(header, items)
message = [header]
items.each do |item|
message << "- #{item}"
end
message.join("\n")
end
def ephemeral_response(message)
response = {
response_type: :ephemeral,
status: 200
}.merge(message)
format_response(response)
end
def in_channel_response(message)
response = {
response_type: :in_channel,
status: 200
}.merge(message)
format_response(response)
end
def format_response(response)
response[:text] = format(response[:text]) if response.has_key?(:text)
if response.has_key?(:attachments)
response[:attachments].each do |attachment|
attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext]
attachment[:text] = format(attachment[:text]) if attachment[:text]
end
end
response
end
# Convert Markdown to slacks format
def format(string)
Slack::Notifier::LinkFormatter.format(string)
end
def resource_url
url_for(
[
@resource.project.namespace.becomes(Namespace),
@resource.project,
@resource
]
)
end
end
end
module Gitlab::ChatCommands::Presenters
class Deploy < Gitlab::ChatCommands::Presenters::Base
def present(from, to)
message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})."
in_channel_response(text: message)
end
def no_actions
ephemeral_response(text: "No action found to be executed")
end
def too_many_actions
ephemeral_response(text: "Too many actions defined")
end
private
def resource_url
polymorphic_url(
[ @resource.project.namespace.becomes(Namespace), @resource.project, @resource]
)
end
end
end
module Gitlab::ChatCommands::Presenters
class Issuable < Gitlab::ChatCommands::Presenters::Base
private
def project
@resource.project
end
def author
@resource.author
end
def fields
[
{
title: "Assignee",
value: @resource.assignee ? @resource.assignee.name : "_None_",
short: true
},
{
title: "Milestone",
value: @resource.milestone ? @resource.milestone.title : "_None_",
short: true
},
{
title: "Labels",
value: @resource.labels.any? ? @resource.label_names : "_None_",
short: true
}
]
end
end
end
module Gitlab::ChatCommands::Presenters
class ListIssues < Gitlab::ChatCommands::Presenters::Base
def present
ephemeral_response(text: "Here are the issues I found:", attachments: attachments)
end
private
def attachments
@resource.map do |issue|
state = issue.open? ? "Open" : "Closed"
{
fallback: "Issue #{issue.to_reference}: #{issue.title}",
color: "#d22852",
text: "[#{issue.to_reference}](#{url_for([namespace, project, issue])}) · #{issue.title} (#{state})",
mrkdwn_in: [
"text"
]
}
end
end
def project
@project ||= @resource.first.project
end
def namespace