webdestroya/lita-github-commits

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

Summary

Maintainability
A
2 hrs
Test Coverage
require "lita"
require 'time'

module Lita
  module Handlers
    class GithubCommits < Handler
      template_root File.expand_path("../../../../templates", __FILE__)


      #todo: change this to lita4 config http://docs.lita.io/releases/4/
      # config :repos, type: Hash, default: {}
      # config :remember_commits_for, type: Integer, default: 1
      # config :github_webhook_secret, type: String, default: nil
      
      def self.default_config(config)
        config.repos = {}
        config.remember_commits_for = 1
        config.github_webhook_secret = nil
      end

      def self.install_routes()
        http.post "/github-commits", :receive
      end
      install_routes()

      REDIS_KEY_PREFIX = "GH_COMMTIS:"
      SHA_ABBREV_LENGTH = 7  #note the regex below needs to match this constant
      def self.install_commands
        route(/commit\/([a-f0-9]{7,})\s?/i, :check_for_commit, command: false,
              help: { "...commit/<SHA1>..." => "Displays the details of commit SHA1 if known (requires at least #{SHA_ABBREV_LENGTH} digits of the SHA)."}
        )
      end
      install_commands

      def check_for_commit(response)
        sha = abbrev_sha(response.match_data[1])
        if sha.nil? || sha.empty?
          #this shouldn't match regex
          response.reply("[GitHub] I need at least #{SHA_ABBREV_LENGTH} characters of the commit SHA") if response.message.command?
        elsif sha.size <= 6 && response.message.command?
          #this shouldn't match regex
          response.reply("[GitHub] Can you be more precise?")
        elsif  (commit=redis.get(REDIS_KEY_PREFIX + sha))
          response.reply(render_template("commit_details", commit: parse_payload(commit)))
        elsif response.message.command?
          response.reply("[GitHub] Sorry Boss, I can't find that commit")
        #else
        #  response.reply("I got nothing to say about #{sha}.")
        end
      end

      def receive(request, response)
        event_type = request.env['HTTP_X_GITHUB_EVENT'] || 'unknown'
        Lita.logger.debug("Received GitHub #{event_type} event") rescue ""
        if !valid_signature(request)
          response.status = 404
        elsif event_type == "push"
          payload = parse_payload(request.body) or return
          store_commits(payload)
          repo = get_repo(payload)
          notify_rooms(repo, payload)
        elsif event_type == "ping"
          response.status = 200
          response.write "Working!"
        else
          response.status = 404
        end
      end

      private

      def valid_signature(request)
        #not actually validating yet, just checking for presence of signature
        #should look something like
        #payload_body = request.body.read
        #signature = 'sha1=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), config.github_webhook_secret, payload_body)
        #Rack::Utils.secure_compare(signature, request.env['HTTP_X_HUB_SIGNATURE'])
        if config.github_webhook_secret.nil? || config.github_webhook_secret.empty? || (request.env['HTTP_X_HUB_SIGNATURE'] && !request.env['HTTP_X_HUB_SIGNATURE'].empty?)
          return true
        else
          Lita.logger.debug("Message sender validation failed") rescue ""
          return false
        end
      end

      def parse_payload(payload)
        MultiJson.load(payload)
      rescue MultiJson::LoadError => e
        Lita.logger.warn("Could not parse JSON payload from Github: #{e.message}")
        return
      end

      def notify_rooms(repo, payload)
        rooms = rooms_for_repo(repo) or return
        message = format_message(payload)

        rooms.each do |room|
          target = Source.new(room: room)
          robot.send_message(target, message)
        end
      end

      def store_commits(payload)
        ttl = remember_commits_for*86400
        return if ttl == 0
        commits = payload['commits']
        branch = branch_from_ref(payload['ref'])
        commits.each do |commit|
          key = REDIS_KEY_PREFIX + commit['id'][0,SHA_ABBREV_LENGTH]
          commit[:branch] = branch
          redis.setex(key,ttl,commit.to_json)
        end
      end

      def format_message(payload)
        commits = payload['commits']
        branch = branch_from_ref(payload['ref'])
        if commits.size > 0
          author = committer_and_author(commits.first)
          messages = commit_messages(commits)
          commit_pluralization = commits.size > 1 ? 'commits' : 'commit'
          "[GitHub] Got #{commits.size} new #{commit_pluralization} #{author} on #{payload['repository']['owner']['name']}/#{payload['repository']['name']} on the #{branch} branch\n" + messages.join("\n")
        elsif payload['created']
          "[GitHub] #{payload['pusher']['name']} created: #{payload['ref']}: #{payload['base_ref']}"
        elsif payload['deleted']
          "[GitHub] #{payload['pusher']['name']} deleted: #{payload['ref']}"
        end
      rescue
        Lita.logger.warn "Error formatting message for payload: #{payload}"
        return
      end

      def abbrev_sha(sha)
        sha.nil? ? nil : sha[0,SHA_ABBREV_LENGTH]
      end

      def branch_from_ref(ref)
        ref.split('/').last
      end

      def committer_and_author(commit)
        if commit['author']['username'] != commit['committer']['username']
          "authored by #{commit['author']['name']} and committed by " +
            "#{commit['committer']['name']}"
        else
          "from #{commit['author']['name']}"
        end
      end

      def commit_messages(commits)
        commits.collect do |commit|
          "  * #{abbrev_sha(commit['id'])}: #{commit['message']}"
        end
      end

      def rooms_for_repo(repo)
        rooms = Lita.config.handlers.github_commits.repos[repo]

        if rooms && rooms.size > 0 
          Array(rooms)
        else
          Lita.logger.warn "Notification from GitHub Commits for unconfigured project: #{repo}"
          return
        end
      end

      def get_repo(payload)
        "#{payload['repository']['owner']['name']}/#{payload['repository']['name']}"
      end
      
      def remember_commits_for
        Lita.config.handlers.github_commits.remember_commits_for
      end
    end

    Lita.register_handler(GithubCommits)
  end
end