summaryrefslogtreecommitdiff
path: root/app/services
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-02 18:18:39 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-02 18:18:39 +0000
commitb9ce0fe1e6311105b7a748126621f9bfbe37fb2e (patch)
treec73b711a72de036cf3f48be9365038fea171c8c6 /app/services
parent6f991190fe4dbb93070b090a9a31d71b25e8101d (diff)
downloadgitlab-ce-b9ce0fe1e6311105b7a748126621f9bfbe37fb2e.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/services')
-rw-r--r--app/services/integrations/slack_event_service.rb61
-rw-r--r--app/services/integrations/slack_events/app_home_opened_service.rb92
-rw-r--r--app/services/integrations/slack_events/url_verification_service.rb26
-rw-r--r--app/services/integrations/slack_interaction_service.rb36
-rw-r--r--app/services/integrations/slack_interactions/block_action_service.rb32
-rw-r--r--app/services/integrations/slack_interactions/incident_management/incident_modal_closed_service.rb58
-rw-r--r--app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb162
-rw-r--r--app/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler.rb131
8 files changed, 598 insertions, 0 deletions
diff --git a/app/services/integrations/slack_event_service.rb b/app/services/integrations/slack_event_service.rb
new file mode 100644
index 00000000000..65f3c226e34
--- /dev/null
+++ b/app/services/integrations/slack_event_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+# Performs the initial handling of event payloads sent from Slack to GitLab.
+# See `API::Integrations::Slack::Events` which calls this service.
+module Integrations
+ class SlackEventService
+ URL_VERIFICATION_EVENT = 'url_verification'
+
+ UnknownEventError = Class.new(StandardError)
+
+ def initialize(params)
+ # When receiving URL verification events, params[:type] is 'url_verification'.
+ # For all other events we subscribe to, params[:type] is 'event_callback' and
+ # the specific type of the event will be in params[:event][:type].
+ # Remove both of these from the params before they are passed to the services.
+ type = params.delete(:type)
+ type = params[:event].delete(:type) if type == 'event_callback'
+
+ @slack_event = type
+ @params = params
+ end
+
+ def execute
+ raise UnknownEventError, "Unable to handle event type: '#{slack_event}'" unless routable_event?
+
+ payload = route_event
+
+ ServiceResponse.success(payload: payload)
+ end
+
+ private
+
+ # The `url_verification` slack_event response must be returned to Slack in-request,
+ # so for this event we call the service directly instead of through a worker.
+ #
+ # All other events must be handled asynchronously in order to return a 2xx response
+ # immediately to Slack in the request. See https://api.slack.com/apis/connections/events-api.
+ def route_in_request?
+ slack_event == URL_VERIFICATION_EVENT
+ end
+
+ def routable_event?
+ route_in_request? || route_to_event_worker?
+ end
+
+ def route_to_event_worker?
+ SlackEventWorker.event?(slack_event)
+ end
+
+ # Returns a payload for the service response.
+ def route_event
+ return SlackEvents::UrlVerificationService.new(params).execute if route_in_request?
+
+ SlackEventWorker.perform_async(slack_event: slack_event, params: params)
+
+ {}
+ end
+
+ attr_reader :slack_event, :params
+ end
+end
diff --git a/app/services/integrations/slack_events/app_home_opened_service.rb b/app/services/integrations/slack_events/app_home_opened_service.rb
new file mode 100644
index 00000000000..48dda324270
--- /dev/null
+++ b/app/services/integrations/slack_events/app_home_opened_service.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+# Handles the Slack `app_home_opened` event sent from Slack to GitLab.
+# Responds with a POST to the Slack API 'views.publish' method.
+#
+# See:
+# - https://api.slack.com/methods/views.publish
+# - https://api.slack.com/events/app_home_opened
+module Integrations
+ module SlackEvents
+ class AppHomeOpenedService
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(params)
+ @slack_user_id = params.dig(:event, :user)
+ @slack_workspace_id = params[:team_id]
+ end
+
+ def execute
+ # Legacy Slack App integrations will not yet have a token we can use
+ # to call the Slack API. Do nothing, and consider the service successful.
+ unless slack_installation
+ logger.info(
+ slack_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id,
+ message: 'SlackInstallation record has no bot token'
+ )
+
+ return ServiceResponse.success
+ end
+
+ begin
+ response = ::Slack::API.new(slack_installation).post(
+ 'views.publish',
+ payload
+ )
+ rescue *Gitlab::HTTP::HTTP_ERRORS => e
+ return ServiceResponse
+ .error(message: 'HTTP exception when calling Slack API')
+ .track_exception(
+ as: e.class,
+ slack_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id
+ )
+ end
+
+ return ServiceResponse.success if response['ok']
+
+ # For a list of errors, see:
+ # https://api.slack.com/methods/views.publish#errors
+ ServiceResponse.error(
+ message: 'Slack API returned an error',
+ payload: response
+ ).track_exception(
+ slack_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id,
+ response: response.to_h
+ )
+ end
+
+ private
+
+ def slack_installation
+ SlackIntegration.with_bot.find_by_team_id(slack_workspace_id)
+ end
+ strong_memoize_attr :slack_installation
+
+ def slack_gitlab_user_connection
+ ChatNames::FindUserService.new(slack_workspace_id, slack_user_id).execute
+ end
+ strong_memoize_attr :slack_gitlab_user_connection
+
+ def payload
+ {
+ user_id: slack_user_id,
+ view: ::Slack::BlockKit::AppHomeOpened.new(
+ slack_user_id,
+ slack_workspace_id,
+ slack_gitlab_user_connection,
+ slack_installation
+ ).build
+ }
+ end
+
+ def logger
+ Gitlab::IntegrationsLogger
+ end
+
+ attr_reader :slack_user_id, :slack_workspace_id
+ end
+ end
+end
diff --git a/app/services/integrations/slack_events/url_verification_service.rb b/app/services/integrations/slack_events/url_verification_service.rb
new file mode 100644
index 00000000000..dbe2ffc77f8
--- /dev/null
+++ b/app/services/integrations/slack_events/url_verification_service.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# Returns the special URL verification response expected by Slack when the
+# GitLab Slack app is first configured to receive Slack events.
+#
+# Slack will issue the challenge request to the endpoint that receives events
+# and expect it to respond with same the `challenge` param back.
+#
+# See https://api.slack.com/apis/connections/events-api.
+module Integrations
+ module SlackEvents
+ class UrlVerificationService
+ def initialize(params)
+ @challenge = params[:challenge]
+ end
+
+ def execute
+ { challenge: challenge }
+ end
+
+ private
+
+ attr_reader :challenge
+ end
+ end
+end
diff --git a/app/services/integrations/slack_interaction_service.rb b/app/services/integrations/slack_interaction_service.rb
new file mode 100644
index 00000000000..30e1a396f0d
--- /dev/null
+++ b/app/services/integrations/slack_interaction_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Integrations
+ class SlackInteractionService
+ UnknownInteractionError = Class.new(StandardError)
+
+ INTERACTIONS = {
+ 'view_closed' => SlackInteractions::IncidentManagement::IncidentModalClosedService,
+ 'view_submission' => SlackInteractions::IncidentManagement::IncidentModalSubmitService,
+ 'block_actions' => SlackInteractions::BlockActionService
+ }.freeze
+
+ def initialize(params)
+ @interaction_type = params.delete(:type)
+ @params = params
+ end
+
+ def execute
+ raise UnknownInteractionError, "Unable to handle interaction type: '#{interaction_type}'" \
+ unless interaction?(interaction_type)
+
+ service_class = INTERACTIONS[interaction_type]
+ service_class.new(params).execute
+
+ ServiceResponse.success
+ end
+
+ private
+
+ attr_reader :interaction_type, :params
+
+ def interaction?(type)
+ INTERACTIONS.key?(type)
+ end
+ end
+end
diff --git a/app/services/integrations/slack_interactions/block_action_service.rb b/app/services/integrations/slack_interactions/block_action_service.rb
new file mode 100644
index 00000000000..d135635fda4
--- /dev/null
+++ b/app/services/integrations/slack_interactions/block_action_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Integrations
+ module SlackInteractions
+ class BlockActionService
+ ALLOWED_UPDATES_HANDLERS = {
+ 'incident_management_project' => SlackInteractions::SlackBlockActions::IncidentManagement::ProjectUpdateHandler
+ }.freeze
+
+ def initialize(params)
+ @params = params
+ end
+
+ def execute
+ actions.each do |action|
+ action_id = action[:action_id]
+
+ action_handler_class = ALLOWED_UPDATES_HANDLERS[action_id]
+ action_handler_class.new(params, action).execute
+ end
+ end
+
+ private
+
+ def actions
+ params[:actions].select { |action| ALLOWED_UPDATES_HANDLERS[action[:action_id]] }
+ end
+
+ attr_accessor :params
+ end
+ end
+end
diff --git a/app/services/integrations/slack_interactions/incident_management/incident_modal_closed_service.rb b/app/services/integrations/slack_interactions/incident_management/incident_modal_closed_service.rb
new file mode 100644
index 00000000000..9daa5d76df7
--- /dev/null
+++ b/app/services/integrations/slack_interactions/incident_management/incident_modal_closed_service.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Integrations
+ module SlackInteractions
+ module IncidentManagement
+ class IncidentModalClosedService
+ def initialize(params)
+ @params = params
+ end
+
+ def execute
+ begin
+ response = close_modal
+ rescue *Gitlab::HTTP::HTTP_ERRORS => e
+ return ServiceResponse
+ .error(message: 'HTTP exception when calling Slack API')
+ .track_exception(
+ params: params,
+ as: e.class
+ )
+ end
+
+ return ServiceResponse.success if response['ok']
+
+ ServiceResponse.error(
+ message: _('Something went wrong while closing the incident form.'),
+ payload: response
+ ).track_exception(
+ response: response.to_h,
+ params: params
+ )
+ end
+
+ private
+
+ attr_accessor :params
+
+ def close_modal
+ request_body = Gitlab::Json.dump(close_request_body)
+ response_url = params.dig(:view, :private_metadata)
+
+ Gitlab::HTTP.post(response_url, body: request_body, headers: headers)
+ end
+
+ def close_request_body
+ {
+ replace_original: 'true',
+ text: _('Incident creation cancelled.')
+ }
+ end
+
+ def headers
+ { 'Content-Type' => 'application/json' }
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb b/app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb
new file mode 100644
index 00000000000..34af03640d3
--- /dev/null
+++ b/app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+module Integrations
+ module SlackInteractions
+ module IncidentManagement
+ class IncidentModalSubmitService
+ include GitlabRoutingHelper
+ include Gitlab::Routing
+
+ IssueCreateError = Class.new(StandardError)
+
+ def initialize(params)
+ @params = params
+ @values = params.dig(:view, :state, :values)
+ @team_id = params.dig(:team, :id)
+ @user_id = params.dig(:user, :id)
+ @additional_message = ''
+ end
+
+ def execute
+ create_response = Issues::CreateService.new(
+ container: project,
+ current_user: find_user.user,
+ params: incident_params,
+ spam_params: nil
+ ).execute
+
+ raise IssueCreateError, create_response.errors.to_sentence if create_response.error?
+
+ incident = create_response.payload[:issue]
+ incident_link = incident_link_text(incident)
+ response = send_to_slack(incident_link)
+
+ return ServiceResponse.success(payload: { incident: incident }) if response['ok']
+
+ ServiceResponse.error(
+ message: _('Something went wrong when sending the incident link to Slack.'),
+ payload: response
+ ).track_exception(
+ response: response.to_h,
+ slack_workspace_id: team_id,
+ slack_user_id: user_id
+ )
+ rescue StandardError => e
+ send_to_slack(_('There was a problem creating the incident. Please try again.'))
+
+ ServiceResponse
+ .error(
+ message: e.message
+ ).track_exception(
+ slack_workspace_id: team_id,
+ slack_user_id: user_id,
+ as: e.class
+ )
+ end
+
+ private
+
+ attr_accessor :params, :values, :team_id, :user_id, :additional_message
+
+ def incident_params
+ {
+ title: values.dig(:title_input, :title, :value),
+ severity: severity,
+ confidential: confidential?,
+ description: description,
+ escalation_status: { status: status },
+ issue_type: "incident",
+ assignee_ids: [assignee],
+ label_ids: labels
+ }
+ end
+
+ def strip_markup(string)
+ SlackMarkdownSanitizer.sanitize(string)
+ end
+
+ def send_to_slack(text)
+ response_url = params.dig(:view, :private_metadata)
+
+ body = {
+ replace_original: 'true',
+ text: text
+ }
+
+ Gitlab::HTTP.post(
+ response_url,
+ body: Gitlab::Json.dump(body),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ def incident_link_text(incident)
+ "#{_('New incident has been created')}: " \
+ "<#{issue_url(incident)}|#{incident.to_reference} " \
+ "- #{strip_markup(incident.title)}>. #{@additional_message}"
+ end
+
+ def project
+ project_id = values.dig(
+ :project_and_severity_selector,
+ :incident_management_project,
+ :selected_option,
+ :value)
+
+ Project.find(project_id)
+ end
+
+ def find_user
+ ChatNames::FindUserService.new(team_id, user_id).execute
+ end
+
+ def description
+ description =
+ values.dig(:incident_description, :description, :value) ||
+ values.dig(project.id.to_s.to_sym, :description, :value)
+
+ zoom_link = values.dig(:zoom, :link, :value)
+
+ return description if zoom_link.blank?
+
+ "#{description} \n/zoom #{zoom_link}"
+ end
+
+ def confidential?
+ values.dig(:confidentiality, :confidential, :selected_options).present?
+ end
+
+ def severity
+ values.dig(:project_and_severity_selector, :severity, :selected_option, :value) || 'unknown'
+ end
+
+ def status
+ values.dig(:status_and_assignee_selector, :status, :selected_option, :value)
+ end
+
+ def assignee
+ assignee_id = values.dig(:status_and_assignee_selector, :assignee, :selected_option, :value)
+
+ return unless assignee_id
+
+ user = User.find_by_id(assignee_id)
+ member = project.member(user)
+
+ unless member
+ @additional_message =
+ "However, " \
+ "#{user.name} was not assigned to the incident as they are not a member in #{project.name}."
+
+ return
+ end
+
+ member.user_id
+ end
+
+ def labels
+ values.dig(:label_selector, :labels, :selected_options)&.pluck(:value)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler.rb b/app/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler.rb
new file mode 100644
index 00000000000..5f24c8ec4f5
--- /dev/null
+++ b/app/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+module Integrations
+ module SlackInteractions
+ module SlackBlockActions
+ module IncidentManagement
+ class ProjectUpdateHandler
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(params, action)
+ @view = params[:view]
+ @action = action
+ @team_id = params.dig(:view, :team_id)
+ @user_id = params.dig(:user, :id)
+ end
+
+ def execute
+ return if project_unchanged?
+ return unless allowed?
+
+ post_updated_modal
+ end
+
+ private
+
+ def allowed?
+ return false unless current_user
+
+ current_user.can?(:read_project, old_project) &&
+ current_user.can?(:read_project, new_project)
+ end
+
+ def current_user
+ ChatNames::FindUserService.new(team_id, user_id).execute&.user
+ end
+ strong_memoize_attr :current_user
+
+ def slack_installation
+ SlackIntegration.with_bot.find_by_team_id(team_id)
+ end
+ strong_memoize_attr :slack_installation
+
+ def post_updated_modal
+ modal = update_modal
+
+ begin
+ response = ::Slack::API.new(slack_installation).post(
+ 'views.update',
+ {
+ view_id: view[:id],
+ view: modal
+ }
+ )
+ rescue *::Gitlab::HTTP::HTTP_ERRORS => e
+ return ServiceResponse
+ .error(message: 'HTTP exception when calling Slack API')
+ .track_exception(
+ as: e.class,
+ slack_workspace_id: view[:team_id]
+ )
+ end
+
+ return ServiceResponse.success(message: _('Modal updated')) if response['ok']
+
+ ServiceResponse.error(
+ message: _('Something went wrong while updating the modal.'),
+ payload: response
+ ).track_exception(
+ response: response.to_h,
+ slack_workspace_id: view[:team_id],
+ slack_user_id: slack_installation.user_id
+ )
+ end
+
+ def update_modal
+ updated_view = update_incident_template
+ cleanup(updated_view)
+ end
+
+ def update_incident_template
+ updated_view = view.dup
+
+ incident_description_blocks = updated_view[:blocks].select do |block|
+ block[:block_id] == 'incident_description' || block[:block_id] == old_project.id.to_s
+ end
+
+ incident_description_blocks.first[:element][:initial_value] = read_template_content
+ incident_description_blocks.first[:block_id] = new_project.id.to_s
+
+ Integrations::SlackInteractions::IncidentManagement::IncidentModalOpenedService
+ .cache_write(view[:id], new_project.id.to_s)
+
+ updated_view
+ end
+
+ def new_project
+ Project.find(action.dig(:selected_option, :value))
+ end
+ strong_memoize_attr :new_project
+
+ def old_project
+ old_project_id = Integrations::SlackInteractions::IncidentManagement::IncidentModalOpenedService
+ .cache_read(view[:id])
+
+ Project.find(old_project_id) if old_project_id
+ end
+ strong_memoize_attr :old_project
+
+ def project_unchanged?
+ old_project == new_project
+ end
+
+ def read_template_content
+ new_project.incident_management_setting&.issue_template_content.to_s
+ end
+
+ def cleanup(view)
+ view.except!(
+ :id, :team_id, :state,
+ :hash, :previous_view_id,
+ :root_view_id, :app_id,
+ :app_installed_team_id,
+ :bot_id)
+ end
+
+ attr_accessor :view, :action, :team_id, :user_id
+ end
+ end
+ end
+ end
+end