summaryrefslogtreecommitdiff
path: root/lib/gitlab/slash_commands
diff options
context:
space:
mode:
authorEric Eastwood <contact@ericeastwood.com>2017-05-31 00:50:53 -0500
committerEric Eastwood <contact@ericeastwood.com>2017-06-15 09:01:56 -0500
commitea090291bba6bb665b3631cc5a2659e6673a6959 (patch)
tree1daf4c15aee8afc0eebef94a345eb077d0390632 /lib/gitlab/slash_commands
parent42aaae9916b7b76da968579fcc722067947df018 (diff)
downloadgitlab-ce-ea090291bba6bb665b3631cc5a2659e6673a6959.tar.gz
Rename "Slash commands" to "Quick actions"
Fix https://gitlab.com/gitlab-org/gitlab-ce/issues/27070 Deprecate "chat commands" in favor of "slash commands" We looked for things like: - `slash commmand` - `slash_command` - `slash-command` - `SlashCommand`
Diffstat (limited to 'lib/gitlab/slash_commands')
-rw-r--r--lib/gitlab/slash_commands/base_command.rb47
-rw-r--r--lib/gitlab/slash_commands/command.rb44
-rw-r--r--lib/gitlab/slash_commands/command_definition.rb89
-rw-r--r--lib/gitlab/slash_commands/deploy.rb50
-rw-r--r--lib/gitlab/slash_commands/dsl.rb140
-rw-r--r--lib/gitlab/slash_commands/extractor.rb122
-rw-r--r--lib/gitlab/slash_commands/help.rb28
-rw-r--r--lib/gitlab/slash_commands/issue_command.rb13
-rw-r--r--lib/gitlab/slash_commands/issue_new.rb42
-rw-r--r--lib/gitlab/slash_commands/issue_search.rb23
-rw-r--r--lib/gitlab/slash_commands/issue_show.rb23
-rw-r--r--lib/gitlab/slash_commands/presenters/access.rb40
-rw-r--r--lib/gitlab/slash_commands/presenters/base.rb77
-rw-r--r--lib/gitlab/slash_commands/presenters/deploy.rb21
-rw-r--r--lib/gitlab/slash_commands/presenters/help.rb27
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_base.rb43
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_new.rb50
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_search.rb47
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_show.rb61
-rw-r--r--lib/gitlab/slash_commands/result.rb5
20 files changed, 641 insertions, 351 deletions
diff --git a/lib/gitlab/slash_commands/base_command.rb b/lib/gitlab/slash_commands/base_command.rb
new file mode 100644
index 00000000000..cc3c9a50555
--- /dev/null
+++ b/lib/gitlab/slash_commands/base_command.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module SlashCommands
+ class BaseCommand
+ QUERY_LIMIT = 5
+
+ def self.match(_text)
+ raise NotImplementedError
+ end
+
+ def self.help_message
+ raise NotImplementedError
+ end
+
+ def self.available?(_project)
+ raise NotImplementedError
+ end
+
+ def self.allowed?(_user, _ability)
+ true
+ end
+
+ def self.can?(object, action, subject)
+ Ability.allowed?(object, action, subject)
+ end
+
+ def execute(_)
+ raise NotImplementedError
+ end
+
+ def collection
+ raise NotImplementedError
+ end
+
+ attr_accessor :project, :current_user, :params
+
+ def initialize(project, user, params = {})
+ @project, @current_user, @params = project, user, params.dup
+ end
+
+ private
+
+ def find_by_iid(iid)
+ collection.find_by(iid: iid)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb
new file mode 100644
index 00000000000..a78408b0519
--- /dev/null
+++ b/lib/gitlab/slash_commands/command.rb
@@ -0,0 +1,44 @@
+module Gitlab
+ module SlashCommands
+ class Command < BaseCommand
+ COMMANDS = [
+ Gitlab::SlashCommands::IssueShow,
+ Gitlab::SlashCommands::IssueNew,
+ Gitlab::SlashCommands::IssueSearch,
+ Gitlab::SlashCommands::Deploy
+ ].freeze
+
+ def execute
+ command, match = match_command
+
+ if command
+ if command.allowed?(project, current_user)
+ command.new(project, current_user, params).execute(match)
+ else
+ Gitlab::SlashCommands::Presenters::Access.new.access_denied
+ end
+ else
+ Gitlab::SlashCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
+ end
+ end
+
+ def match_command
+ match = nil
+ service =
+ available_commands.find do |klass|
+ match = klass.match(params[:text])
+ end
+
+ [service, match]
+ end
+
+ private
+
+ def available_commands
+ COMMANDS.select do |klass|
+ klass.available?(project)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/command_definition.rb b/lib/gitlab/slash_commands/command_definition.rb
deleted file mode 100644
index caab8856014..00000000000
--- a/lib/gitlab/slash_commands/command_definition.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-module Gitlab
- module SlashCommands
- class CommandDefinition
- attr_accessor :name, :aliases, :description, :explanation, :params,
- :condition_block, :parse_params_block, :action_block
-
- def initialize(name, attributes = {})
- @name = name
-
- @aliases = attributes[:aliases] || []
- @description = attributes[:description] || ''
- @explanation = attributes[:explanation] || ''
- @params = attributes[:params] || []
- @condition_block = attributes[:condition_block]
- @parse_params_block = attributes[:parse_params_block]
- @action_block = attributes[:action_block]
- end
-
- def all_names
- [name, *aliases]
- end
-
- def noop?
- action_block.nil?
- end
-
- def available?(opts)
- return true unless condition_block
-
- context = OpenStruct.new(opts)
- context.instance_exec(&condition_block)
- end
-
- def explain(context, opts, arg)
- return unless available?(opts)
-
- if explanation.respond_to?(:call)
- execute_block(explanation, context, arg)
- else
- explanation
- end
- end
-
- def execute(context, opts, arg)
- return if noop? || !available?(opts)
-
- execute_block(action_block, context, arg)
- end
-
- def to_h(opts)
- context = OpenStruct.new(opts)
-
- desc = description
- if desc.respond_to?(:call)
- desc = context.instance_exec(&desc) rescue ''
- end
-
- prms = params
- if prms.respond_to?(:call)
- prms = Array(context.instance_exec(&prms)) rescue params
- end
-
- {
- name: name,
- aliases: aliases,
- description: desc,
- params: prms
- }
- end
-
- private
-
- def execute_block(block, context, arg)
- if arg.present?
- parsed = parse_params(arg, context)
- context.instance_exec(parsed, &block)
- elsif block.arity == 0
- context.instance_exec(&block)
- end
- end
-
- def parse_params(arg, context)
- return arg unless parse_params_block
-
- context.instance_exec(arg, &parse_params_block)
- end
- end
- end
-end
diff --git a/lib/gitlab/slash_commands/deploy.rb b/lib/gitlab/slash_commands/deploy.rb
new file mode 100644
index 00000000000..e71eb15d604
--- /dev/null
+++ b/lib/gitlab/slash_commands/deploy.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ module SlashCommands
+ class Deploy < BaseCommand
+ def self.match(text)
+ /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
+ end
+
+ def self.help_message
+ 'deploy <environment> to <target-environment>'
+ end
+
+ def self.available?(project)
+ project.builds_enabled?
+ end
+
+ def self.allowed?(project, user)
+ can?(user, :create_deployment, project)
+ end
+
+ def execute(match)
+ from = match[:from]
+ to = match[:to]
+
+ actions = find_actions(from, to)
+
+ if actions.none?
+ Gitlab::SlashCommands::Presenters::Deploy.new(nil).no_actions
+ elsif actions.one?
+ action = play!(from, to, actions.first)
+ Gitlab::SlashCommands::Presenters::Deploy.new(action).present(from, to)
+ else
+ Gitlab::SlashCommands::Presenters::Deploy.new(actions).too_many_actions
+ end
+ end
+
+ private
+
+ def play!(from, to, action)
+ action.play(current_user)
+ end
+
+ def find_actions(from, to)
+ environment = project.environments.find_by(name: from)
+ return [] unless environment
+
+ environment.actions_for(to).select(&:starts_environment?)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/dsl.rb b/lib/gitlab/slash_commands/dsl.rb
deleted file mode 100644
index 1b5b4566d81..00000000000
--- a/lib/gitlab/slash_commands/dsl.rb
+++ /dev/null
@@ -1,140 +0,0 @@
-module Gitlab
- module SlashCommands
- module Dsl
- extend ActiveSupport::Concern
-
- included do
- cattr_accessor :command_definitions, instance_accessor: false do
- []
- end
-
- cattr_accessor :command_definitions_by_name, instance_accessor: false do
- {}
- end
- end
-
- class_methods do
- # Allows to give a description to the next slash command.
- # This description is shown in the autocomplete menu.
- # It accepts a block that will be evaluated with the context given to
- # `CommandDefintion#to_h`.
- #
- # Example:
- #
- # desc do
- # "This is a dynamic description for #{noteable.to_ability_name}"
- # end
- # command :command_key do |arguments|
- # # Awesome code block
- # end
- def desc(text = '', &block)
- @description = block_given? ? block : text
- end
-
- # Allows to define params for the next slash command.
- # These params are shown in the autocomplete menu.
- #
- # Example:
- #
- # params "~label ~label2"
- # command :command_key do |arguments|
- # # Awesome code block
- # end
- def params(*params, &block)
- @params = block_given? ? block : params
- end
-
- # Allows to give an explanation of what the command will do when
- # executed. This explanation is shown when rendering the Markdown
- # preview.
- #
- # Example:
- #
- # explanation do |arguments|
- # "Adds label(s) #{arguments.join(' ')}"
- # end
- # command :command_key do |arguments|
- # # Awesome code block
- # end
- def explanation(text = '', &block)
- @explanation = block_given? ? block : text
- end
-
- # Allows to define conditions that must be met in order for the command
- # to be returned by `.command_names` & `.command_definitions`.
- # It accepts a block that will be evaluated with the context given to
- # `CommandDefintion#to_h`.
- #
- # Example:
- #
- # condition do
- # project.public?
- # end
- # command :command_key do |arguments|
- # # Awesome code block
- # end
- def condition(&block)
- @condition_block = block
- end
-
- # Allows to perform initial parsing of parameters. The result is passed
- # both to `command` and `explanation` blocks, instead of the raw
- # parameters.
- # It accepts a block that will be evaluated with the context given to
- # `CommandDefintion#to_h`.
- #
- # Example:
- #
- # parse_params do |raw|
- # raw.strip
- # end
- # command :command_key do |parsed|
- # # Awesome code block
- # end
- def parse_params(&block)
- @parse_params_block = block
- end
-
- # Registers a new command which is recognizeable from body of email or
- # comment.
- # It accepts aliases and takes a block.
- #
- # Example:
- #
- # command :my_command, :alias_for_my_command do |arguments|
- # # Awesome code block
- # end
- def command(*command_names, &block)
- name, *aliases = command_names
-
- definition = CommandDefinition.new(
- name,
- aliases: aliases,
- description: @description,
- explanation: @explanation,
- params: @params,
- condition_block: @condition_block,
- parse_params_block: @parse_params_block,
- action_block: block
- )
-
- self.command_definitions << definition
-
- definition.all_names.each do |name|
- self.command_definitions_by_name[name] = definition
- end
-
- @description = nil
- @explanation = nil
- @params = nil
- @condition_block = nil
- @parse_params_block = nil
- end
-
- def definition_by_name(name)
- command_definitions_by_name[name.to_sym]
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/slash_commands/extractor.rb b/lib/gitlab/slash_commands/extractor.rb
deleted file mode 100644
index 6dbb467d70d..00000000000
--- a/lib/gitlab/slash_commands/extractor.rb
+++ /dev/null
@@ -1,122 +0,0 @@
-module Gitlab
- module SlashCommands
- # This class takes an array of commands that should be extracted from a
- # given text.
- #
- # ```
- # extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels])
- # ```
- class Extractor
- attr_reader :command_definitions
-
- def initialize(command_definitions)
- @command_definitions = command_definitions
- end
-
- # Extracts commands from content and return an array of commands.
- # The array looks like the following:
- # [
- # ['command1'],
- # ['command3', 'arg1 arg2'],
- # ]
- # The command and the arguments are stripped.
- # The original command text is removed from the given `content`.
- #
- # Usage:
- # ```
- # extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels])
- # msg = %(hello\n/labels ~foo ~"bar baz"\nworld)
- # commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
- # msg #=> "hello\nworld"
- # ```
- def extract_commands(content, opts = {})
- return [content, []] unless content
-
- content = content.dup
-
- commands = []
-
- content.delete!("\r")
- content.gsub!(commands_regex(opts)) do
- if $~[:cmd]
- commands << [$~[:cmd], $~[:arg]].reject(&:blank?)
- ''
- else
- $~[0]
- end
- end
-
- [content.strip, commands]
- end
-
- private
-
- # Builds a regular expression to match known commands.
- # First match group captures the command name and
- # second match group captures its arguments.
- #
- # It looks something like:
- #
- # /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
- def commands_regex(opts)
- names = command_names(opts).map(&:to_s)
-
- @commands_regex ||= %r{
- (?<code>
- # Code blocks:
- # ```
- # Anything, including `/cmd arg` which are ignored by this filter
- # ```
-
- ^```
- .+?
- \n```$
- )
- |
- (?<html>
- # HTML block:
- # <tag>
- # Anything, including `/cmd arg` which are ignored by this filter
- # </tag>
-
- ^<[^>]+?>\n
- .+?
- \n<\/[^>]+?>$
- )
- |
- (?<html>
- # Quote block:
- # >>>
- # Anything, including `/cmd arg` which are ignored by this filter
- # >>>
-
- ^>>>
- .+?
- \n>>>$
- )
- |
- (?:
- # Command not in a blockquote, blockcode, or HTML tag:
- # /close
-
- ^\/
- (?<cmd>#{Regexp.union(names)})
- (?:
- [ ]
- (?<arg>[^\n]*)
- )?
- (?:\n|$)
- )
- }mx
- end
-
- def command_names(opts)
- command_definitions.flat_map do |command|
- next if command.noop?
-
- command.all_names
- end.compact
- end
- end
- end
-end
diff --git a/lib/gitlab/slash_commands/help.rb b/lib/gitlab/slash_commands/help.rb
new file mode 100644
index 00000000000..81f3707e03e
--- /dev/null
+++ b/lib/gitlab/slash_commands/help.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module SlashCommands
+ 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, text)
+ Gitlab::SlashCommands::Presenters::Help.new(commands).present(trigger, text)
+ end
+
+ def trigger
+ params[:command]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/issue_command.rb b/lib/gitlab/slash_commands/issue_command.rb
new file mode 100644
index 00000000000..87ea19b8806
--- /dev/null
+++ b/lib/gitlab/slash_commands/issue_command.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module SlashCommands
+ class IssueCommand < BaseCommand
+ def self.available?(project)
+ project.issues_enabled? && project.default_issues_tracker?
+ end
+
+ def collection
+ IssuesFinder.new(current_user, project_id: project.id).execute
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/issue_new.rb b/lib/gitlab/slash_commands/issue_new.rb
new file mode 100644
index 00000000000..25f965e843d
--- /dev/null
+++ b/lib/gitlab/slash_commands/issue_new.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module SlashCommands
+ class IssueNew < IssueCommand
+ def self.match(text)
+ # we can not match \n with the dot by passing the m modifier as than
+ # the title and description are not seperated
+ /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
+ end
+
+ def self.help_message
+ 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>'
+ end
+
+ def self.allowed?(project, user)
+ can?(user, :create_issue, project)
+ end
+
+ def execute(match)
+ title = match[:title]
+ description = match[:description].to_s.rstrip
+
+ issue = create_issue(title: title, description: description)
+
+ if issue.persisted?
+ presenter(issue).present
+ else
+ presenter(issue).display_errors
+ end
+ end
+
+ private
+
+ def create_issue(title:, description:)
+ Issues::CreateService.new(project, current_user, title: title, description: description).execute
+ end
+
+ def presenter(issue)
+ Gitlab::SlashCommands::Presenters::IssueNew.new(issue)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/issue_search.rb b/lib/gitlab/slash_commands/issue_search.rb
new file mode 100644
index 00000000000..acba84b54b4
--- /dev/null
+++ b/lib/gitlab/slash_commands/issue_search.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module SlashCommands
+ class IssueSearch < IssueCommand
+ def self.match(text)
+ /\Aissue\s+search\s+(?<query>.*)/.match(text)
+ end
+
+ def self.help_message
+ "issue search <your query>"
+ end
+
+ def execute(match)
+ issues = collection.search(match[:query]).limit(QUERY_LIMIT)
+
+ if issues.present?
+ Presenters::IssueSearch.new(issues).present
+ else
+ Presenters::Access.new(issues).not_found
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/issue_show.rb b/lib/gitlab/slash_commands/issue_show.rb
new file mode 100644
index 00000000000..ffa5184e5cb
--- /dev/null
+++ b/lib/gitlab/slash_commands/issue_show.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module SlashCommands
+ class IssueShow < IssueCommand
+ def self.match(text)
+ /\Aissue\s+show\s+#{Issue.reference_prefix}?(?<iid>\d+)/.match(text)
+ end
+
+ def self.help_message
+ "issue show <id>"
+ end
+
+ def execute(match)
+ issue = find_by_iid(match[:iid])
+
+ if issue
+ Gitlab::SlashCommands::Presenters::IssueShow.new(issue).present
+ else
+ Gitlab::SlashCommands::Presenters::Access.new.not_found
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/access.rb b/lib/gitlab/slash_commands/presenters/access.rb
new file mode 100644
index 00000000000..1a817eb735b
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/access.rb
@@ -0,0 +1,40 @@
+module Gitlab
+ module SlashCommands
+ module Presenters
+ class Access < 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
+
+ def unknown_command(commands)
+ ephemeral_response(text: help_message(trigger))
+ end
+
+ private
+
+ def help_message(trigger)
+ header_with_list("Command not found, these are the commands you can use", full_commands(trigger))
+ end
+
+ def full_commands(trigger)
+ @resource.map { |command| "#{trigger} #{command.help_message}" }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/base.rb b/lib/gitlab/slash_commands/presenters/base.rb
new file mode 100644
index 00000000000..27696436574
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/base.rb
@@ -0,0 +1,77 @@
+module Gitlab
+ module SlashCommands
+ module 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.key?(:text)
+
+ if response.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
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/deploy.rb b/lib/gitlab/slash_commands/presenters/deploy.rb
new file mode 100644
index 00000000000..b8dc77bd37b
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/deploy.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module SlashCommands
+ module Presenters
+ class Deploy < 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
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/help.rb b/lib/gitlab/slash_commands/presenters/help.rb
new file mode 100644
index 00000000000..ea611a4d629
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/help.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module SlashCommands
+ module Presenters
+ class Help < Presenters::Base
+ def present(trigger, text)
+ ephemeral_response(text: help_message(trigger, text))
+ end
+
+ private
+
+ def help_message(trigger, text)
+ return "No commands available :thinking_face:" unless @resource.present?
+
+ if text.start_with?('help')
+ header_with_list("Available commands", full_commands(trigger))
+ else
+ header_with_list("Unknown command, these commands are available", full_commands(trigger))
+ end
+ end
+
+ def full_commands(trigger)
+ @resource.map { |command| "#{trigger} #{command.help_message}" }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/issue_base.rb b/lib/gitlab/slash_commands/presenters/issue_base.rb
new file mode 100644
index 00000000000..341f2aabdd0
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/issue_base.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module SlashCommands
+ module Presenters
+ module IssueBase
+ def color(issuable)
+ issuable.open? ? '#38ae67' : '#d22852'
+ end
+
+ def status_text(issuable)
+ issuable.open? ? 'Open' : 'Closed'
+ end
+
+ def project
+ @resource.project
+ end
+
+ def author
+ @resource.author
+ end
+
+ def fields
+ [
+ {
+ title: "Assignee",
+ value: @resource.assignees.any? ? @resource.assignees.first.name : "_None_",
+ short: true
+ },
+ {
+ title: "Milestone",
+ value: @resource.milestone ? @resource.milestone.title : "_None_",
+ short: true
+ },
+ {
+ title: "Labels",
+ value: @resource.labels.any? ? @resource.label_names.join(', ') : "_None_",
+ short: true
+ }
+ ]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/issue_new.rb b/lib/gitlab/slash_commands/presenters/issue_new.rb
new file mode 100644
index 00000000000..86490a39cc1
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/issue_new.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ module SlashCommands
+ module Presenters
+ class IssueNew < Presenters::Base
+ include Presenters::IssueBase
+
+ def present
+ in_channel_response(new_issue)
+ end
+
+ private
+
+ def new_issue
+ {
+ attachments: [
+ {
+ title: "#{@resource.title} · #{@resource.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "New issue #{@resource.to_reference}: #{@resource.title}",
+ pretext: pretext,
+ color: color(@resource),
+ fields: fields,
+ mrkdwn_in: [
+ :title,
+ :pretext,
+ :text,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def pretext
+ "I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}"
+ end
+
+ def project_link
+ "[#{project.name_with_namespace}](#{project.web_url})"
+ end
+
+ def author_profile_link
+ "[#{author.to_reference}](#{url_for(author)})"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/issue_search.rb b/lib/gitlab/slash_commands/presenters/issue_search.rb
new file mode 100644
index 00000000000..4e27d668685
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/issue_search.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module SlashCommands
+ module Presenters
+ class IssueSearch < Presenters::Base
+ include Presenters::IssueBase
+
+ def present
+ text = if @resource.count >= 5
+ "Here are the first 5 issues I found:"
+ elsif @resource.one?
+ "Here is the only issue I found:"
+ else
+ "Here are the #{@resource.count} issues I found:"
+ end
+
+ ephemeral_response(text: text, attachments: attachments)
+ end
+
+ private
+
+ def attachments
+ @resource.map do |issue|
+ url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})"
+
+ {
+ color: color(issue),
+ fallback: "#{issue.to_reference} #{issue.title}",
+ text: "#{url} · #{issue.title} (#{status_text(issue)})",
+
+ mrkdwn_in: [
+ :text
+ ]
+ }
+ end
+ end
+
+ def project
+ @project ||= @resource.first.project
+ end
+
+ def namespace
+ @namespace ||= project.namespace.becomes(Namespace)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/issue_show.rb b/lib/gitlab/slash_commands/presenters/issue_show.rb
new file mode 100644
index 00000000000..c99316df667
--- /dev/null
+++ b/lib/gitlab/slash_commands/presenters/issue_show.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module SlashCommands
+ module Presenters
+ class IssueShow < Presenters::Base
+ include Presenters::IssueBase
+
+ def present
+ if @resource.confidential?
+ ephemeral_response(show_issue)
+ else
+ in_channel_response(show_issue)
+ end
+ end
+
+ private
+
+ def show_issue
+ {
+ attachments: [
+ {
+ title: "#{@resource.title} · #{@resource.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "Issue #{@resource.to_reference}: #{@resource.title}",
+ pretext: pretext,
+ text: text,
+ color: color(@resource),
+ fields: fields,
+ mrkdwn_in: [
+ :pretext,
+ :text,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def text
+ message = "**#{status_text(@resource)}**"
+
+ if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero?
+ return message
+ end
+
+ message << " · "
+ message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero?
+ message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero?
+ message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero?
+
+ message
+ end
+
+ def pretext
+ "Issue *#{@resource.to_reference}* from #{project.name_with_namespace}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/result.rb b/lib/gitlab/slash_commands/result.rb
new file mode 100644
index 00000000000..7021b4b01b2
--- /dev/null
+++ b/lib/gitlab/slash_commands/result.rb
@@ -0,0 +1,5 @@
+module Gitlab
+ module SlashCommands
+ Result = Struct.new(:type, :message)
+ end
+end