issue and mergerequest slash command for mattermost
This commit includes a couple of thing: - A chatops controller - Mattermost::CommandService - Mattermost::Commands::(IssueService|MergeRequestService) The controller is the point where mattermost, and later slack will have to fire their payload to. This in turn will execute the CommandService. Thats where the authentication and authorization should happen. So far this is not yet implemented. This should happen in later commits. Per subcommand, in case of `/gitlab issue show 123` issue whould be the subcommand, there is a service to parse the data, and fetch the resource. The resource is passed back to the CommandService which structures the data.
This commit is contained in:
parent
c2e6f5fc24
commit
9d8ca60d22
12 changed files with 455 additions and 1 deletions
10
app/controllers/chat_ops_controller.rb
Normal file
10
app/controllers/chat_ops_controller.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
class ChatOpsController < ApplicationController
|
||||
respond_to :json
|
||||
|
||||
skip_before_action :verify_authenticity_token
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
def trigger
|
||||
render json: { ok: true }
|
||||
end
|
||||
end
|
|
@ -23,7 +23,9 @@ class Project < ActiveRecord::Base
|
|||
|
||||
cache_markdown_field :description, pipeline: :description
|
||||
|
||||
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
|
||||
delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
|
||||
:merge_requests_enabled?, :issues_enabled?, to: :project_feature,
|
||||
allow_nil: true
|
||||
|
||||
default_value_for :archived, false
|
||||
default_value_for :visibility_level, gitlab_config_features.visibility_level
|
||||
|
|
|
@ -60,6 +60,10 @@ class ProjectFeature < ActiveRecord::Base
|
|||
merge_requests_access_level > DISABLED
|
||||
end
|
||||
|
||||
def issues_enabled?
|
||||
issues_access_level > DISABLED
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Validates builds and merge requests access level
|
||||
|
|
74
app/services/mattermost/command_service.rb
Normal file
74
app/services/mattermost/command_service.rb
Normal file
|
@ -0,0 +1,74 @@
|
|||
module Mattermost
|
||||
class CommandService < BaseService
|
||||
SERVICES = [
|
||||
Mattermost::Commands::IssueService,
|
||||
Mattermost::Commands::MergeRequestService
|
||||
]
|
||||
|
||||
def execute
|
||||
return unknown_user unless current_user
|
||||
return not_found_404 unless can?(current_user, :read_project, project)
|
||||
|
||||
triggered_command = command
|
||||
service = SERVICES.find do |service|
|
||||
service.triggered_by?(triggered_command) && service.available?(project)
|
||||
end
|
||||
|
||||
if service
|
||||
present service.new(project, current_user, params).execute
|
||||
else
|
||||
help_message
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def command
|
||||
params[:text].match(/\A(?<command>\S+)/)[:command]
|
||||
end
|
||||
|
||||
def present(result)
|
||||
return not_found_404 unless result
|
||||
|
||||
if result.respond_to?(:count)
|
||||
if count > 1
|
||||
#TODO
|
||||
return resource_list(result)
|
||||
else
|
||||
result = result.first
|
||||
end
|
||||
end
|
||||
|
||||
message = "### [#{result.to_reference} #{result.title}](link(result))"
|
||||
message << "\n\n#{result.description}" if result.description
|
||||
|
||||
{
|
||||
response_type: :in_channel,
|
||||
text: message
|
||||
}
|
||||
end
|
||||
|
||||
def unknown_user
|
||||
{
|
||||
response_type: :ephemeral,
|
||||
text: 'Hi there! I have not yet had the pleasure to get acquainted!' # TODO allow user to authenticate and authorize
|
||||
}
|
||||
end
|
||||
|
||||
def not_found_404
|
||||
{
|
||||
response_type: :ephemeral,
|
||||
text: "404 not found! GitLab couldn't find what your were looking for! :boom:",
|
||||
}
|
||||
end
|
||||
|
||||
def help_message
|
||||
command_help_messages = SERVICES.map { |service| service.help_message(project) }
|
||||
|
||||
{
|
||||
response_type: :ephemeral,
|
||||
text: "Sadly, the used command does not exist, lets take a look at your options here:\n\n#{command_help_messages.join("\n")}"
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
60
app/services/mattermost/commands/base_service.rb
Normal file
60
app/services/mattermost/commands/base_service.rb
Normal file
|
@ -0,0 +1,60 @@
|
|||
module Mattermost
|
||||
module Commands
|
||||
class BaseService < ::BaseService
|
||||
class << self
|
||||
def triggered_by?(_)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def available?(_)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def help_message(_)
|
||||
NotImplementedError
|
||||
end
|
||||
end
|
||||
|
||||
QUERY_LIMIT = 5
|
||||
|
||||
def execute
|
||||
subcommand, args = parse_command
|
||||
|
||||
if subcommands.include?(subcommand)
|
||||
send(subcommand, args)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This method can only be used by a resource that has an iid. Also, the
|
||||
# class should implement #collection itself. Probably project.resource
|
||||
# would suffice
|
||||
def show(args)
|
||||
iid = args.first
|
||||
|
||||
result = collection.find_by(iid: iid)
|
||||
if readable?(result)
|
||||
result
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Child class should implement #collection
|
||||
def search(args)
|
||||
query = args.join(' ')
|
||||
|
||||
collection.search(query).limit(QUERY_LIMIT).select do |issuable|
|
||||
readable?(issuable)
|
||||
end
|
||||
end
|
||||
|
||||
def command
|
||||
params[:text]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
49
app/services/mattermost/commands/issue_service.rb
Normal file
49
app/services/mattermost/commands/issue_service.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
module Mattermost
|
||||
module Commands
|
||||
class IssueService < Mattermost::Commands::BaseService
|
||||
class << self
|
||||
def triggered_by?(command)
|
||||
command == 'issue'
|
||||
end
|
||||
|
||||
def available?(project)
|
||||
project.issues_enabled? && project.default_issues_tracker?
|
||||
end
|
||||
|
||||
def help_message(project)
|
||||
return nil unless available?(project)
|
||||
|
||||
message = "issue show <issue_id>"
|
||||
message << "issue search <query>"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
#TODO implement create
|
||||
def subcommands
|
||||
%w[creates search show]
|
||||
end
|
||||
|
||||
def collection
|
||||
project.issues
|
||||
end
|
||||
|
||||
def readable?(issue)
|
||||
can?(current_user, :read_issue, issue)
|
||||
end
|
||||
|
||||
# 'issue create my new title\nmy new description
|
||||
# => 'create', ['my', 'new', 'title, ['my new description']]
|
||||
# 'issue show 123'
|
||||
# => 'show', ['123']
|
||||
def parse_command
|
||||
split = command.split
|
||||
subcommand = split[1]
|
||||
args = split[2..-1]
|
||||
|
||||
return subcommand, args
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
46
app/services/mattermost/commands/merge_request_service.rb
Normal file
46
app/services/mattermost/commands/merge_request_service.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
module Mattermost
|
||||
module Commands
|
||||
class MergeRequestService < Mattermost::Commands::BaseService
|
||||
class << self
|
||||
def triggered_by?(command)
|
||||
command == 'mergerequest'
|
||||
end
|
||||
|
||||
def available?(project)
|
||||
project.merge_requests_enabled?
|
||||
end
|
||||
|
||||
def help_message(project)
|
||||
return nil unless available?(project)
|
||||
|
||||
message = "mergerequest show <merge request id>\n"
|
||||
message << "mergerequest search <query>"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def subcommands
|
||||
%w[show search]
|
||||
end
|
||||
|
||||
def collection
|
||||
project.merge_requests
|
||||
end
|
||||
|
||||
def readable?(_)
|
||||
can?(current_user, :read_merge_request, project)
|
||||
end
|
||||
|
||||
# 'mergerequest show 123' => 'show', ['123']
|
||||
# 'mergerequest search my query' => 'search',['my', 'query']
|
||||
def parse_command
|
||||
split = command.split
|
||||
subcommand = split[1]
|
||||
args = split[2..-1]
|
||||
|
||||
return subcommand, args
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -68,6 +68,11 @@ Rails.application.routes.draw do
|
|||
# Notification settings
|
||||
resources :notification_settings, only: [:create, :update]
|
||||
|
||||
# For slash commands to POST
|
||||
namespace :chat_ops do
|
||||
post :trigger
|
||||
end
|
||||
|
||||
draw :import
|
||||
draw :uploads
|
||||
draw :explore
|
||||
|
|
11
spec/controllers/chat_ops_controller_spec.rb
Normal file
11
spec/controllers/chat_ops_controller_spec.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ChatOpsController, type: :controller do
|
||||
describe "POST #trigger" do
|
||||
it "returns http success" do
|
||||
post :trigger
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
55
spec/services/mattermost/command_service_spec.rb
Normal file
55
spec/services/mattermost/command_service_spec.rb
Normal file
|
@ -0,0 +1,55 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Mattermost::CommandService, service: true do
|
||||
let(:project) { build(:project) }
|
||||
let(:user) { build(:user) }
|
||||
let(:params) { { text: 'issue show 1' } }
|
||||
|
||||
subject { described_class.new(project, user, params).execute }
|
||||
|
||||
describe '#execute' do
|
||||
context 'no user could be found' do
|
||||
let(:user) { nil }
|
||||
|
||||
it 'asks the user to introduce him/herself' do
|
||||
expect(subject[:response_type]).to be :ephemeral
|
||||
expect(subject[:text]).to start_with 'Hi there!'
|
||||
end
|
||||
end
|
||||
|
||||
context 'no project could be found' do
|
||||
it 'shows a 404 not found message' do
|
||||
expect(subject[:response_type]).to be :ephemeral
|
||||
expect(subject[:text]).to start_with '404 not found!'
|
||||
end
|
||||
end
|
||||
|
||||
context 'the user has access to the project' do
|
||||
let(:project) { create(:project) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
project.team << [user, :master]
|
||||
end
|
||||
|
||||
context 'no command service is triggered' do
|
||||
let(:params) { { text: 'unknown_command' } }
|
||||
|
||||
it 'shows the help messages' do
|
||||
expect(subject[:response_type]).to be :ephemeral
|
||||
expect(subject[:text]).to start_with 'Sadly, the used command'
|
||||
end
|
||||
end
|
||||
|
||||
context 'a valid command is executed' do
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
let(:params) { { text: "issue show #{issue.iid}" } }
|
||||
|
||||
it 'a resource is presented to the user' do
|
||||
expect(subject[:response_type]).to be :in_channel
|
||||
expect(subject[:text]).to match issue.title
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
70
spec/services/mattermost/commands/issue_service_spec.rb
Normal file
70
spec/services/mattermost/commands/issue_service_spec.rb
Normal file
|
@ -0,0 +1,70 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Mattermost::Commands::IssueService do
|
||||
let(:project) { create(:project) }
|
||||
let(:issue ) { create(:issue, :confidential, title: 'Bird is the word', project: project) }
|
||||
let(:user) { issue.author }
|
||||
|
||||
subject { described_class.new(project, user, params).execute }
|
||||
|
||||
before do
|
||||
project.team << [user, :developer]
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'show as subcommand' do
|
||||
context 'issue can be found' do
|
||||
let(:params) { { text: "issue show #{issue.iid}" } }
|
||||
|
||||
it 'returns the merge request' do
|
||||
expect(subject).to eq issue
|
||||
end
|
||||
|
||||
context 'the user has no access' do
|
||||
let(:non_member) { create(:user) }
|
||||
subject { described_class.new(project, non_member, params).execute }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'issue can not be found' do
|
||||
let(:params) { { text: 'issue show 12345' } }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'search as a subcommand' do
|
||||
context 'with results' do
|
||||
let(:params) { { text: "issue search is the word" } }
|
||||
|
||||
it 'returns the issue' do
|
||||
expect(subject).to eq [issue]
|
||||
end
|
||||
end
|
||||
|
||||
context 'without results' do
|
||||
let(:params) { { text: 'issue search mepmep' } }
|
||||
|
||||
it 'returns an empty collection' do
|
||||
expect(subject).to eq []
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'help_message' do
|
||||
context 'issues are disabled' do
|
||||
it 'returns nil' do
|
||||
allow(described_class).to receive(:available?).and_return false
|
||||
|
||||
expect(described_class.help_message(project)).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,68 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Mattermost::Commands::MergeRequestService do
|
||||
let(:project) { create(:project, :private) }
|
||||
let(:merge_request) { create(:merge_request, title: 'Bird is the word', source_project: project) }
|
||||
let(:user) { merge_request.author }
|
||||
|
||||
subject { described_class.new(project, user, params).execute }
|
||||
|
||||
before do
|
||||
project.team << [user, :developer]
|
||||
end
|
||||
|
||||
context 'show as subcommand' do
|
||||
context 'merge request can be found' do
|
||||
let(:params) { { text: "mergerequest show #{merge_request.iid}" } }
|
||||
|
||||
it 'returns the merge request' do
|
||||
expect(subject).to eq merge_request
|
||||
end
|
||||
|
||||
context 'the user has no access' do
|
||||
let(:non_member) { create(:user) }
|
||||
subject { described_class.new(project, non_member, params).execute }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'merge request can not be found' do
|
||||
let(:params) { { text: 'mergerequest show 12345' } }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'search as a subcommand' do
|
||||
context 'with results' do
|
||||
let(:params) { { text: "mergerequest search is the word" } }
|
||||
|
||||
it 'returns the merge_request' do
|
||||
expect(subject).to eq [merge_request]
|
||||
end
|
||||
end
|
||||
|
||||
context 'without results' do
|
||||
let(:params) { { text: 'mergerequest search mepmep' } }
|
||||
|
||||
it 'returns an empty collection' do
|
||||
expect(subject).to eq []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'help_message' do
|
||||
context 'issues are disabled' do
|
||||
it 'returns nil' do
|
||||
allow(described_class).to receive(:available?).and_return false
|
||||
|
||||
expect(described_class.help_message(project)).to eq nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue