lgaticaq/hubot-travis-ci-hook

View on GitHub
src/script.js

Summary

Maintainability
D
2 days
Test Coverage
B
80%
// Description
//   Clone travis integration with slack
//
// Dependencies:
//   crypto, parse-ms
//
// Configuration:
//   TRAVIS_PRO, TRAVIS_SHORT
//
// Commands:
//   None
//
// Author:
//   lgaticaq

'use strict'

const crypto = require('crypto')
const parseMs = require('parse-ms')

const icons = [
  'https://cdn.travis-ci.com/images/logos/TravisCI-Mascot-1-20feeadb48fc2492ba741d89cb5a5c8a.png',
  'https://cdn.travis-ci.com/images/logos/TravisCI-Mascot-2-05e0a25826cabd8cd4ad265f9e47b0b5.png',
  'https://cdn.travis-ci.com/images/logos/TravisCI-Mascot-3-53205345fb60d55134faf2871bf4394f.png',
  'https://cdn.travis-ci.com/images/logos/TravisCI-Mascot-4-364f3fca6c400bceab00bdf565b17af1.png',
  'https://cdn.travis-ci.com/images/logos/TravisCI-Mascot-grey-8a0cb7d8c3aa57b9b1e981c0e3f1db13.png',
  'https://cdn.travis-ci.com/images/logos/TravisCI-Mascot-blue-50a7eace1d5009af5b1229b03c5b2775.png',
  'https://cdn.travis-ci.com/images/logos/TravisCI-Mascot-red-ceeabb77262f6d2203fe7c3635b77b98.png',
  'https://cdn.travis-ci.com/images/logos/TravisCI-Mascot-pride-f062dd7e4391eea78a6a164ddc770754.png',
  'https://cdn.travis-ci.com/images/logos/TravisCI-Mascot-pride-4-deadef9f32270ef494b860e76aa366e8.png',
  'https://cdn.travis-ci.com/images/logos/Tessa-1-bdfba3fb6a74f87385afbdecd397f97e.png',
  'https://cdn.travis-ci.com/images/logos/Tessa-2-4913e90413586105249b4f55ca622ec8.png',
  'https://cdn.travis-ci.com/images/logos/Tessa-3-bba95a9bf2298f829b7c08e0af8f9c46.png',
  'https://cdn.travis-ci.com/images/logos/Tessa-4-42d2e224f0ab57afc32c74fac2653853.png',
  'https://cdn.travis-ci.com/images/logos/Tessa-pride-d7cfbd39009645bbecd005212aa1d338.png',
  'https://cdn.travis-ci.com/images/logos/Tessa-pride-4-49f04b06d4009a791ea88d198934da29.png'
]

const verifySignature = (signature, payload, publicKey) => {
  const verifier = crypto.createVerify('sha1')
  verifier.update(payload)
  return verifier.verify(publicKey, signature, 'base64')
}

const request = (robot, uri) => {
  return new Promise((resolve, reject) => {
    robot
      .http(uri)
      .header('User-Agent', robot.name)
      .get()((err, res, body) => {
        if (err) {
          return reject(err)
        } else if (res.statusCode !== 200) {
          return reject(new Error(`Error response code ${res.statusCode}`))
        } else {
          try {
            return resolve(JSON.parse(body))
          } catch (err) {
            return reject(err)
          }
        }
      })
  })
}

const getPublicKey = robot => {
  const domain = process.env.TRAVIS_PRO === 'true' ? '.com' : '.org'
  const uri = `https://api.travis-ci${domain}/config`
  return request(robot, uri).then(data => {
    return data.config.notifications.webhook.public_key
  })
}

const getAuthor = (robot, owner, name, sha) => {
  const uri = `https://api.github.com/repos/${owner}/${name}/commits/${sha}`
  return request(robot, uri).then(data => {
    return {
      name: data.commit.author.name,
      url: data.author.html_url,
      avatar: data.author.avatar_url,
      commit: data.html_url
    }
  })
}

const getAttachments = (robot, req) => {
  return new Promise((resolve, reject) => {
    try {
      let hours, minutes, seconds
      const signature = req.get('Signature') || ''
      const { payload } = req.body
      const data = JSON.parse(payload)
      const sha = data.commit.substr(0, 7)
      const ms = parseMs(data.duration * 1000)
      const days = ms.days > 0 ? `${ms.days} d` : ''
      if (ms.hours > 0) {
        if (days === '') {
          hours = `${ms.hours} h`
        } else {
          hours = ` ${ms.hours} h`
        }
      } else {
        hours = ''
      }
      if (ms.minutes > 0) {
        if (hours === '') {
          minutes = `${ms.minutes} min`
        } else {
          minutes = ` ${ms.minutes} min`
        }
      } else {
        minutes = ''
      }
      if (ms.seconds > 0) {
        if (minutes === '') {
          seconds = `${ms.seconds} sec`
        } else {
          seconds = ` ${ms.seconds} sec`
        }
      } else {
        seconds = ''
      }
      const duration = `${days}${hours}${minutes}${seconds}`
      const icon = icons[Math.floor(Math.random() * icons.length)]
      const color = (() => {
        switch (data.status_message) {
          case 'Pending':
            return 'warning'
          case 'Passed':
            return 'good'
          case 'Fixed':
            return 'good'
          case 'Broken':
            return 'danger'
          case 'Failed':
            return 'danger'
          case 'Still Failing':
            return 'danger'
          default:
            return 'warning'
        }
      })()
      const promises = [
        getPublicKey(robot).then(publicKey =>
          verifySignature(signature, payload, publicKey)
        ),
        getAuthor(
          robot,
          data.repository.owner_name,
          data.repository.name,
          data.commit
        )
      ]
      Promise.all(promises)
        .then(results => {
          const [isValid, author] = Array.from(results)
          const fallback =
            `Build #${data.number} (${sha}) of ` +
            `${data.repository.owner_name}/${data.repository.name}` +
            `@${data.branch} by ${author.name} ` +
            `${data.status_message.toLowerCase()} in ${duration}`
          const message =
            `Build <${data.build_url}|#${data.number}> ` +
            `(<${data.compare_url}|${sha}>) of ` +
            `${data.repository.owner_name}/${data.repository.name}` +
            `@${data.branch} by ` +
            `<${author.url}|${author.name}> ` +
            `${data.status_message.toLowerCase()} in ${duration}`
          const compare = data.compare_url
            .split('/compare/')
            .reverse()
            .shift()
          const full = {
            as_user: false,
            link_names: 1,
            icon_url: icon,
            username: 'Travis CI',
            attachments: [
              {
                fallback,
                color,
                author_name: author.name,
                author_link: author.url,
                author_icon: author.avatar,
                title: `${data.status_message} Build #${data.number}`,
                title_link: data.build_url,
                text: data.message,
                fields: [
                  {
                    title: 'Branch',
                    value: `${data.branch}`,
                    short: true
                  },
                  {
                    title: 'Commit',
                    value: `<${author.commit}|${sha}>`,
                    short: true
                  },
                  {
                    title: 'Compare',
                    value: `<${data.compare_url}|${compare}>`,
                    short: true
                  },
                  {
                    title: 'Duration',
                    value: duration,
                    short: true
                  }
                ]
              }
            ]
          }
          const short = {
            as_user: false,
            link_names: 1,
            icon_url: icon,
            username: 'Travis CI',
            attachments: [
              {
                fallback,
                color,
                text: message
              }
            ]
          }
          return resolve({
            isValid,
            fallback,
            full,
            short
          })
        })
        .catch(err => reject(err))
    } catch (err) {
      return reject(err)
    }
  })
}

module.exports = robot => {
  robot.router.post('/travis-ci/:room', (req, res) => {
    const channel = req.params.room
    getAttachments(robot, req)
      .then(results => {
        if (!results.isValid) return
        if (['SlackBot', 'Room'].includes(robot.adapter.constructor.name)) {
          if (process.env.TRAVIS_SHORT === 'true') {
            robot.adapter.client.web.chat.postMessage(
              `#${channel}`,
              null,
              results.short
            )
            res.send('Ok')
          } else {
            robot.adapter.client.web.chat.postMessage(
              `#${channel}`,
              null,
              results.full
            )
            res.send('Ok')
          }
        } else {
          robot.messageRoom(channel, results.fallback)
          res.send('Ok')
        }
      })
      .catch(err => {
        robot.emit('error', err)
        res.send('Error')
      })
  })
}