esigler/lita-jira

View on GitHub
lib/lita/handlers/jira.rb

Summary

Maintainability
A
45 mins
Test Coverage
# frozen_string_literal: true

# lita-jira plugin
module Lita
  # Because we can.
  module Handlers
    # Main handler
    # rubocop:disable Metrics/ClassLength
    class Jira < Handler
      namespace 'Jira'

      config :username, required: true, type: String
      config :password, required: true, type: String
      config :site, required: true, type: String
      config :context, required: false, type: String, default: ''
      config :format, required: false, type: String, default: 'verbose'
      config :ambient, required: false, types: [TrueClass, FalseClass], default: false
      config :ignore, required: false, type: Array, default: []
      config :rooms, required: false, type: Array
      config :use_ssl, required: false, types: [TrueClass, FalseClass], default: true
      config :points_field, required: false, type: String

      include ::JiraHelper::Issue
      include ::JiraHelper::Misc
      include ::JiraHelper::Regex
      include ::JiraHelper::Utility

      route(
        /^jira\s#{ISSUE_PATTERN}$/,
        :summary,
        command: true,
        help: {
          t('help.summary.syntax') => t('help.summary.desc')
        }
      )

      route(
        /^jira\sdetails\s#{ISSUE_PATTERN}$/,
        :details,
        command: true,
        help: {
          t('help.details.syntax') => t('help.details.desc')
        }
      )

      route(
        /^jira\smyissues$/,
        :myissues,
        command: true,
        help: {
          t('help.myissues.syntax') => t('help.myissues.desc')
        }
      )

      route(
        /^jira\scomment\son\s#{ISSUE_PATTERN}\s#{COMMENT_PATTERN}$/,
        :comment,
        command: true,
        help: {
          t('help.comment.syntax') => t('help.comment.desc')
        }
      )

      route(
        /^todo\s#{PROJECT_PATTERN}\s#{SUBJECT_PATTERN}(\s#{SUMMARY_PATTERN})?$/,
        :todo,
        command: true,
        help: {
          t('help.todo.syntax') => t('help.todo.desc')
        }
      )

      route(
        /^jira\spoint\s#{ISSUE_PATTERN}\sas\s#{POINTS_PATTERN}$/,
        :point,
        command: true,
        help: {
          t('help.point.syntax') => t('help.point.desc')
        }
      )

      # Detect ambient JIRA issues in non-command messages
      route AMBIENT_PATTERN, :ambient, command: false

      def summary(response)
        issue = fetch_issue(response.match_data['issue'])
        return response.reply(t('error.request')) unless issue
        response.reply(t('issue.summary', key: issue.key, summary: issue.summary))
      end

      def details(response)
        issue = fetch_issue(response.match_data['issue'])
        return response.reply(t('error.request')) unless issue
        response.reply(format_issue(issue))
      end

      def comment(response)
        issue = fetch_issue(response.match_data['issue'])
        return response.reply(t('error.request')) unless issue
        comment = issue.comments.build
        comment.save!(body: response.match_data['comment'])
        response.reply(t('comment.added', issue: issue.key))
      end

      def todo(response)
        issue = create_issue(response.match_data['project'],
                             response.match_data['subject'],
                             response.match_data['summary'])
        return response.reply(t('error.request')) unless issue
        response.reply(t('issue.created', key: issue.key))
      end

      # rubocop:disable Metrics/AbcSize
      def point(response)
        return response.reply(t('error.field_undefined')) if config.points_field.blank?
        issue = fetch_issue(response.match_data['issue'])
        return response.reply(t('error.request')) unless issue
        set_points_on_issue(issue, response)
      end

      def myissues(response)
        return response.reply(t('error.not_identified')) unless user_stored?(response.user)

        begin
          issues = fetch_issues("assignee = '#{get_email(response.user)}' AND status not in (Closed)")
        rescue StandardError => e
          log.error("JIRA HTTPError #{e}")
          response.reply(t('error.request'))
          return
        end

        return response.reply(t('myissues.empty')) if issues.empty?

        response.reply(format_issues(issues))
      end

      def ambient(response)
        return if invalid_ambient?(response)

        # response.matches returns an array of array of strings, where the inner arrays are [issue, project]
        # (e.g. [["XYZ-123", "XYZ"]]). We map it into an array of issues (["XYZ-123"]).
        issue_keys = response.matches.map { |match| match[0] }

        if issue_keys.length > 1
          # Note that if any of the issue keys do not exist in JIRA, then an exception is thrown and no results are returned.
          # A JIRA 'suggestion' has been filed to allow partial results: https://jira.atlassian.com/browse/JRASERVER-40245
          jql = "key in (#{issue_keys.join(',')})"
          # Exceptions are suppressed and no results are returned since this is just ambient parsing and we do not want
          # the bot to pop up with error messages when an explicit command was not requested.
          issues = fetch_issues(jql, true)
          response.reply(format_issues(issues)) if issues && !issues.empty?
        else
          # Only one issue key was parsed, so directly fetch the one issue.
          issue = fetch_issue(response.match_data['issue'], false)
          response.reply(format_issue(issue)) if issue
        end
      end

      private

      def invalid_ambient?(response)
        response.message.command? || !config.ambient || ignored?(response.user) || (config.rooms && !config.rooms.include?(response.message.source.room))
      end

      def ignored?(user)
        config.ignore.include?(user.id) || config.ignore.include?(user.mention_name) || config.ignore.include?(user.name)
      end

      def set_points_on_issue(issue, response)
        points = response.match_data['points']
        begin
          issue.save!(fields: { config.points_field.to_sym => points.to_i })
        rescue StandardError
          return response.reply(t('error.unable_to_point'))
        end
        response.reply(t('point.added', issue: issue.key, points: points))
      end
      # rubocop:enable Metrics/AbcSize
    end
    # rubocop:enable Metrics/ClassLength
    Lita.register_handler(Jira)
  end
end