├── .gitignore ├── script ├── test └── bootstrap ├── src ├── adapters │ ├── index.coffee │ ├── slack.coffee │ └── generic.coffee ├── github │ ├── index.coffee │ ├── pullrequest.coffee │ ├── pullrequests.coffee │ └── webhook.coffee ├── config.coffee ├── utils.coffee ├── reminders.coffee └── index.coffee ├── index.coffee ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # bootstrap environment 4 | source script/bootstrap 5 | 6 | mocha --compilers coffee:coffee-script 7 | -------------------------------------------------------------------------------- /src/adapters/index.coffee: -------------------------------------------------------------------------------- 1 | Slack = require "./slack" 2 | Generic = require "./generic" 3 | 4 | module.exports = { 5 | Slack 6 | Generic 7 | } 8 | -------------------------------------------------------------------------------- /src/github/index.coffee: -------------------------------------------------------------------------------- 1 | PullRequests = require "./pullrequests" 2 | PullRequest = require "./pullrequest" 3 | Webhook = require "./webhook" 4 | 5 | module.exports = { 6 | PullRequests 7 | PullRequest 8 | Webhook 9 | } 10 | -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | module.exports = (robot, scripts) -> 5 | scriptsPath = path.resolve(__dirname, 'src') 6 | fs.exists scriptsPath, (exists) -> 7 | robot.loadFile(scriptsPath, 'index.coffee') if exists 8 | 9 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make sure everything is development forever 4 | export NODE_ENV=development 5 | 6 | # Load environment specific environment variables 7 | if [ -f .env ]; then 8 | source .env 9 | fi 10 | 11 | if [ -f .env.${NODE_ENV} ]; then 12 | source .env.${NODE_ENV} 13 | fi 14 | 15 | npm install 16 | 17 | # Make sure coffee and mocha are on the path 18 | export PATH="node_modules/.bin:$PATH" 19 | -------------------------------------------------------------------------------- /src/adapters/slack.coffee: -------------------------------------------------------------------------------- 1 | _ = require "underscore" 2 | 3 | GenericAdapter = require "./generic" 4 | 5 | class Slack extends GenericAdapter 6 | constructor: (@robot) -> 7 | super @robot 8 | 9 | send: (context, message) -> 10 | payload = {} 11 | if _(message).isString() 12 | payload.text = message 13 | else 14 | payload = _(payload).extend message 15 | @robot.adapter.send room: context.message.room, payload 16 | 17 | module.exports = Slack 18 | -------------------------------------------------------------------------------- /src/config.coffee: -------------------------------------------------------------------------------- 1 | class Config 2 | @debug: process.env.HUBOT_GITHUB_DEBUG 3 | 4 | @github: 5 | url: process.env.HUBOT_GITHUB_URL or "https://api.github.com" 6 | token: process.env.HUBOT_GITHUB_TOKEN 7 | organization: process.env.HUBOT_GITHUB_ORG 8 | webhook: secret: process.env.HUBOT_GITHUB_WEBHOOK_SECRET 9 | 10 | @maps: 11 | repos: JSON.parse process.env.HUBOT_GITHUB_REPOS_MAP if process.env.HUBOT_GITHUB_REPOS_MAP 12 | 13 | unless Config.maps.repos 14 | throw new Error "You must specify a room->repo mapping in the environment as HUBOT_GITHUB_REPOS_MAP, see README.md for details" 15 | 16 | module.exports = Config 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 hubot-scripts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-github-bot", 3 | "description": "A hubot script to list and recurrently remind you about open pull requests and pull request assignments.", 4 | "version": "4.2.1", 5 | "authors": [ 6 | "Nino D'Aversa " 7 | ], 8 | "license": "MIT", 9 | "keywords": [ 10 | "hubot", 11 | "hubot-scripts", 12 | "slack", 13 | "github", 14 | "pull-requests", 15 | "pulls" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:ndaversa/hubot-github-bot.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/ndaversa/hubot-github-bot/issues" 23 | }, 24 | "dependencies": { 25 | "coffee-script": "^1.10.0", 26 | "cron": "^1.0.9", 27 | "fuse.js": "^2.0.1", 28 | "moment": "^2.10.6", 29 | "octokat": "^0.5.0-beta.0", 30 | "underscore": "^1.8.3" 31 | }, 32 | "main": "index.coffee", 33 | "directories": { 34 | "test": "test" 35 | }, 36 | "scripts": { 37 | "test": "echo \"Error: no test specified\" && exit 1" 38 | }, 39 | "homepage": "https://github.com/ndaversa/hubot-github-bot", 40 | "devDependencies": {}, 41 | "author": "Nino D'Aversa (http://ndaversa.com)" 42 | } 43 | -------------------------------------------------------------------------------- /src/github/pullrequest.coffee: -------------------------------------------------------------------------------- 1 | moment = require "moment" 2 | Octokat = require "octokat" 3 | 4 | Config = require "../config" 5 | Utils = require "../utils" 6 | 7 | octo = new Octokat token: Config.github.token 8 | 9 | class PullRequest 10 | @fromUrl: (url) -> 11 | octo.fromUrl(url).fetch() 12 | .then (pr) -> 13 | new PullRequest pr 14 | 15 | constructor: (json, @assignee) -> 16 | @[k] = v for k,v of json when k isnt "assignee" 17 | 18 | toAttachment: -> 19 | color: "#ff9933" 20 | author_name: @user.login 21 | author_icon: @user.avatarUrl 22 | author_link: @user.htmlUrl 23 | title: @title 24 | title_link: @htmlUrl 25 | fields: [ 26 | title: "Updated" 27 | value: moment(@updatedAt).fromNow() 28 | short: yes 29 | , 30 | title: "Status" 31 | value: if @mergeable then "Mergeable" else "Unresolved Conflicts" 32 | short: yes 33 | , 34 | title: "Assignee" 35 | value: if @assignee then "<@#{@assignee.id}>" else "Unassigned" 36 | short: yes 37 | , 38 | title: "Lines" 39 | value: "+#{@additions} -#{@deletions}" 40 | short: yes 41 | ] 42 | fallback: """ 43 | *#{@title}* +#{@additions} -#{@deletions} 44 | Updated: *#{moment(@updatedAt).fromNow()}* 45 | Status: #{if @mergeable then "Mergeable" else "Unresolved Conflicts"} 46 | Author: #{@user.login} 47 | Assignee: #{if @assignee then "#{@assignee.name}" else "Unassigned"} 48 | """ 49 | 50 | module.exports = PullRequest 51 | -------------------------------------------------------------------------------- /src/utils.coffee: -------------------------------------------------------------------------------- 1 | _ = require "underscore" 2 | Fuse = require "fuse.js" 3 | 4 | class Utils 5 | @robot: null 6 | 7 | @findRoom: (msg) -> 8 | room = msg.envelope.room 9 | if _.isUndefined(room) 10 | room = msg.envelope.user.reply_to 11 | room 12 | 13 | @getRoom: (context) -> 14 | room = @robot.adapter.client.rtm.dataStore.getChannelOrGroupByName context.message.room 15 | room = @robot.adapter.client.rtm.dataStore.getChannelGroupOrDMById context.message.room unless room 16 | room = @robot.adapter.client.rtm.dataStore.getDMByUserId context.message.room unless room 17 | room = @robot.adapter.client.rtm.dataStore.getDMByName context.message.room unless room 18 | room 19 | 20 | @getUsers: -> 21 | Utils.robot.adapter?.client?.rtm?.dataStore?.users or Utils.robot.brain.users() 22 | 23 | @lookupUserWithGithub: (github) -> 24 | return Promise.resolve() unless github 25 | 26 | findMatch = (user) -> 27 | name = user.name or user.login 28 | return unless name 29 | users = Utils.getUsers() 30 | users = _(users).keys().map (id) -> 31 | u = users[id] 32 | id: u.id 33 | name: u.name 34 | real_name: u.real_name 35 | 36 | f = new Fuse users, 37 | keys: ['real_name'] 38 | shouldSort: yes 39 | verbose: no 40 | threshold: 0.55 41 | 42 | results = f.search name 43 | result = if results? and results.length >=1 then results[0] else undefined 44 | return Promise.resolve result 45 | 46 | if github.fetch? 47 | github.fetch().then findMatch 48 | else 49 | findMatch github 50 | 51 | module.exports = Utils 52 | -------------------------------------------------------------------------------- /src/github/pullrequests.coffee: -------------------------------------------------------------------------------- 1 | _ = require "underscore" 2 | 3 | Octokat = require "octokat" 4 | Config = require "../config" 5 | PullRequest = require "./pullrequest" 6 | Utils = require "../utils" 7 | 8 | octo = new Octokat 9 | token: Config.github.token 10 | rootUrl: Config.github.url 11 | 12 | class PullRequests 13 | 14 | @openForRoom: (room, user) -> 15 | repos = Config.maps.repos[room.name] 16 | return Promise.reject "There is no github repository associated with this room. Contact your friendly <@#{robot.name}> administrator for assistance" unless repos 17 | 18 | Promise.all( repos.map (repo) -> 19 | repo = octo.repos(Config.github.organization, repo) 20 | repo.pulls.fetch(state: "open") 21 | .then (json) -> 22 | return Promise.all json.items.map (pr) -> 23 | if user? 24 | return if not pr.assignee? 25 | github = octo.fromUrl(pr.assignee.url) 26 | return Utils.lookupUserWithGithub(github).then (assignee) -> 27 | return if user.toLowerCase() isnt assignee?.name.toLowerCase() 28 | return repo.pulls(pr.number).fetch() 29 | else 30 | return repo.pulls(pr.number).fetch() 31 | .then (prs) -> 32 | return Promise.all prs.map (pr) -> 33 | return if not pr 34 | github = octo.fromUrl(pr.assignee.url) if pr.assignee?.url 35 | assignee = Utils.lookupUserWithGithub github 36 | return Promise.all [ pr, assignee ] 37 | ) 38 | .then (repos) -> 39 | pullRequests = [] 40 | for repo in repos 41 | for p in repo when p 42 | pullRequests.push new PullRequest p[0], p[1] 43 | Utils.robot.emit "GithubPullRequestsOpenForRoom", pullRequests, room 44 | pullRequests 45 | .catch ( error ) -> 46 | Utils.robot.logger.error error 47 | Promise.reject error 48 | 49 | module.exports = PullRequests 50 | -------------------------------------------------------------------------------- /src/reminders.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore' 2 | moment = require 'moment' 3 | cronJob = require("cron").CronJob 4 | 5 | class Reminders 6 | constructor: (@robot, @key, @cb) -> 7 | @robot.brain.once 'loaded', => 8 | # Run a cron job that runs every minute, Monday-Friday 9 | new cronJob('0 * * * * 1-5', @_check.bind(@), null, true) 10 | 11 | _get: -> 12 | @robot.brain.get(@key) or [] 13 | 14 | _save: (reminders) -> 15 | @robot.brain.set @key, reminders 16 | 17 | _check: -> 18 | reminders = @_get() 19 | _.chain(reminders).filter(@_shouldFire).pluck('room').each @cb 20 | 21 | _shouldFire: (reminder) -> 22 | now = new Date 23 | currentHours = now.getHours() 24 | currentMinutes = now.getMinutes() 25 | reminderHours = reminder.time.split(':')[0] 26 | reminderMinutes = reminder.time.split(':')[1] 27 | try 28 | reminderHours = parseInt reminderHours, 10 29 | reminderMinutes = parseInt reminderMinutes, 10 30 | catch _error 31 | return false 32 | if reminderHours is currentHours and reminderMinutes is currentMinutes 33 | return true 34 | return false 35 | 36 | getAll: -> 37 | @_get() 38 | 39 | getForRoom: (room) -> 40 | _.where @_get(), room: room 41 | 42 | save: (room, time) -> 43 | reminders = @_get() 44 | newReminder = 45 | time: time 46 | room: room 47 | reminders.push newReminder 48 | @_save reminders 49 | 50 | clearAllForRoom: (room) -> 51 | reminders = @_get() 52 | remindersToKeep = _.reject(reminders, room: room) 53 | @_save remindersToKeep 54 | reminders.length - (remindersToKeep.length) 55 | 56 | clearForRoomAtTime: (room, time) -> 57 | reminders = @_get() 58 | remindersToKeep = _.reject reminders, 59 | room: room 60 | time: time 61 | @_save remindersToKeep 62 | reminders.length - (remindersToKeep.length) 63 | 64 | module.exports = Reminders 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hubot Github Bot 2 | A hubot script to list and recurrently remind you about open pull requests. 3 | Optionally receive direct messages when you are assigned to a pull 4 | request in your organization or for a specific repo or set of repos. 5 | 6 | ###Dependencies 7 | - coffeescript 8 | - cron 9 | - octokat 10 | - moment 11 | - underscore 12 | - fuse.js 13 | 14 | ###Configuration 15 | - `HUBOT_GITHUB_TOKEN` - Github Application Token 16 | - `HUBOT_GITHUB_WEBHOOK_SECRET` - Optional, if you are using webhooks and have a secret set this for additional security checks on payload delivery 17 | - `HUBOT_GITHUB_URL` - Set this value if you are using Github Enterprise default: `https://api.github.com` 18 | - `HUBOT_GITHUB_ORG` - Github Organization Name (the one in the url) 19 | - `HUBOT_GITHUB_REPOS_MAP` eg.`"{"web":["frontend","web"],"android":["android"],"ios":["ios"],"platform":["web"]}"` 20 | 21 | Note: As of v3.3 the `HUBOT_GITHUB_REPOS_MAP` now expects an array for 22 | the value in the map to support multiple repos per channel 23 | 24 | ###Commands 25 | - hubot github open - Shows a list of open pull requests for the repo of this room 26 | - hubot github reminder hh:mm - I'll remind about open pull requests in this room at hh:mm every weekday. 27 | - hubot github list reminders - See all pull request reminders for this room. 28 | - hubot github reminders in every room - Be nosey and see when other rooms have their reminders set 29 | - hubot github delete hh:mm reminder - If you have a reminder at hh:mm, I'll delete it. 30 | - hubot github delete all reminders - Deletes all reminders for this room. 31 | 32 | ####Notifications via Webhooks 33 | In order to receive github notifications you will need to setup a github 34 | webhook for either your entire organization or per repository. You can 35 | find instructions to do so on [Github's website](https://developer.github.com/webhooks/creating/). 36 | You will need your hubot to be reachable from the outside world for this 37 | to work. GithubBot is listening on `/hubot/github-events`. Currently 38 | the following notifications are available: 39 | 40 | * Pull Request Assignment (please ensure the webhook sends pull request events for this to work) 41 | 42 | Note: in order for direct messages (notifications) to work GithubBot attempts to 43 | fuzzy match the github name or github login to someone on your team. It 44 | has been tested primarily on the [Hubot Slack adapter](https://github.com/slackhq/hubot-slack), but it should work 45 | elsewhere. If GithubBot cannot find a matching user it drops the 46 | notification and logs the failure to the console. 47 | -------------------------------------------------------------------------------- /src/adapters/generic.coffee: -------------------------------------------------------------------------------- 1 | _ = require "underscore" 2 | 3 | class GenericAdapter 4 | @GITHUB_NOTIFICATIONS_DISABLED: "github-notifications-disabled" 5 | @GITHUB_DM_COUNTS: "github-dm-counts" 6 | 7 | constructor: (@robot) -> 8 | @disabledUsers = null 9 | @dmCounts = null 10 | 11 | @robot.brain.once "loaded", => 12 | @disabledUsers = @robot.brain.get(GenericAdapter.GITHUB_NOTIFICATIONS_DISABLED) or [] 13 | @dmCounts = @robot.brain.get(GenericAdapter.GITHUB_DM_COUNTS) or {} 14 | 15 | disableNotificationsFor: (user) -> 16 | @robot.logger.info "Disabling Github notifications for #{user.name}" 17 | @disabledUsers.push user.id 18 | @robot.brain.set GenericAdapter.GITHUB_NOTIFICATIONS_DISABLED, _(@disabledUsers).unique() 19 | @robot.brain.save() 20 | 21 | enableNotificationsFor: (user) -> 22 | @robot.logger.info "Enabling Github notifications for #{user.name}" 23 | @disabledUsers = _(@disabledUsers).without user.id 24 | @robot.brain.set GenericAdapter.GITHUB_NOTIFICATIONS_DISABLED, @disabledUsers 25 | @robot.brain.save() 26 | 27 | incrementDMCountFor: (user) -> 28 | return unless @dmCounts? 29 | return unless user?.id? 30 | 31 | @dmCounts[user.id] ||= 0 32 | @dmCounts[user.id]++ 33 | @robot.brain.set GenericAdapter.GITHUB_DM_COUNTS, @dmCounts 34 | @robot.brain.save() 35 | 36 | getDMCountFor: (user) -> 37 | return 0 unless @dmCounts? 38 | return 0 unless user?.id? 39 | @dmCounts[user.id] ||= 0 40 | return @dmCounts[user.id] 41 | 42 | send: (context, message) -> 43 | if _(message).isString() 44 | payload = message 45 | else if message.text or message.attachments 46 | payload = "" 47 | 48 | if message.text 49 | payload += "#{message.text}\n" 50 | 51 | if message.attachments 52 | for attachment in message.attachments 53 | payload += "#{attachment.fallback}\n" 54 | else 55 | return @robot.logger.error "Unable to find a message to send", message 56 | 57 | @robot.send room: context.message.room, payload 58 | 59 | dm: (users, message) -> 60 | users = [ users ] unless _(users).isArray() 61 | for user in users when user 62 | if _(@disabledUsers).contains user.id 63 | @robot.logger.debug "Github Notification surpressed for #{user.name}" 64 | else 65 | if message.author? and user.name is message.author.name 66 | @robot.logger.debug "Github Notification surpressed for #{user.name} because it would be a self-notification" 67 | continue 68 | message.text += "\n#{message.footer}" if message.text and message.footer and @getDMCountFor(user) < 3 69 | @send message: room: user.id, _(message).pick "attachments", "text" 70 | @incrementDMCountFor user 71 | 72 | getPermalink: (msg) -> "" 73 | 74 | module.exports = GenericAdapter 75 | -------------------------------------------------------------------------------- /src/github/webhook.coffee: -------------------------------------------------------------------------------- 1 | Config = require "../config" 2 | Octokat = require "octokat" 3 | Utils = require "../utils" 4 | PullRequest = require "./pullrequest" 5 | 6 | url = require "url" 7 | crypto = require "crypto" 8 | 9 | octo = new Octokat 10 | token: Config.github.token 11 | rootUrl: Config.github.url 12 | 13 | class Webhook 14 | constructor: (@robot) -> 15 | @robot.router.post "/hubot/github-events", (req, res) => 16 | return unless req.body? 17 | hmac = crypto.createHmac "sha1", Config.github.webhook.secret if Config.github.webhook.secret 18 | if hmac and hubSignature = req.headers["x-hub-signature"] 19 | hmac.update JSON.stringify req.body 20 | signature = "sha1=#{hmac.digest "hex"}" 21 | unless signature is hubSignature 22 | return @robot.logger.error "Github Webhook Signature did not match, aborting" 23 | 24 | event = req.body 25 | 26 | if req.headers["x-github-event"] is "pull_request_review" 27 | @onPullRequestReviewAction event 28 | else if req.body.pull_request 29 | @onPullRequest event 30 | 31 | res.send 'OK' 32 | 33 | onPullRequest: (event) -> 34 | switch event.action 35 | when "assigned" 36 | @onPullRequestAssignment event 37 | when "review_requested" 38 | @onPullRequestReviewRequested event 39 | 40 | onPullRequestReviewAction: (event) -> 41 | return unless event.action is "edited" or event.action is "submitted" 42 | return unless event.pull_request?.user?.url? 43 | 44 | user = null 45 | sender = null 46 | Utils.lookupUserWithGithub(octo.fromUrl(event.pull_request.user.url)) 47 | .then (u) -> 48 | user = u 49 | Utils.lookupUserWithGithub(octo.fromUrl(event.sender.url)) 50 | .then (s) -> 51 | sender = s 52 | .catch (error) -> 53 | Utils.robot.logger.error "Github Webhook: Unable to find webhook sender #{event.sender.login}" 54 | .then -> 55 | PullRequest.fromUrl event.pull_request.url 56 | .then (pr) -> 57 | pr.creator = user 58 | @robot.emit "GithubPullRequestReviewed", pr, sender 59 | .catch (error) -> 60 | Utils.robot.logger.error "Github Webhook: Unable to find user to send notification to #{event.pull_request.user.login}" 61 | Utils.robot.logger.error error 62 | Utils.robot.logger.error error.stack 63 | 64 | onPullRequestAssignment: (event) -> 65 | return unless event.assignee?.url? 66 | 67 | user = null 68 | sender = null 69 | Utils.lookupUserWithGithub(octo.fromUrl(event.assignee.url)) 70 | .then (u) -> 71 | user = u 72 | Utils.lookupUserWithGithub(octo.fromUrl(event.sender.url)) 73 | .then (s) -> 74 | sender = s 75 | .catch (error) -> 76 | Utils.robot.logger.error "Github Webhook: Unable to find webhook sender #{event.sender.login}" 77 | .then -> 78 | PullRequest.fromUrl event.pull_request.url 79 | .then (pr) -> 80 | pr.assignee = user 81 | @robot.emit "GithubPullRequestAssigned", pr, sender 82 | .catch (error) -> 83 | Utils.robot.logger.error "Github Webhook: Unable to find user to send notification to #{event.assignee.login}" 84 | Utils.robot.logger.error error 85 | Utils.robot.logger.error error.stack 86 | 87 | onPullRequestReviewRequested: (event) -> 88 | return unless event.requested_reviewer?.url? 89 | 90 | user = null 91 | sender = null 92 | Utils.lookupUserWithGithub(octo.fromUrl(event.requested_reviewer.url)) 93 | .then (u) -> 94 | user = u 95 | Utils.lookupUserWithGithub(octo.fromUrl(event.sender.url)) 96 | .then (s) -> 97 | sender = s 98 | .catch (error) -> 99 | Utils.robot.logger.error "Github Webhook: Unable to find webhook sender #{event.sender.login}" 100 | .then -> 101 | PullRequest.fromUrl event.pull_request.url 102 | .then (pr) -> 103 | pr.requested_reviewer = user 104 | @robot.emit "GithubPullRequestReviewRequested", pr, sender 105 | .catch (error) -> 106 | Utils.robot.logger.error "Github Webhook: Unable to find user to send notification to #{event.requested_reviewer.login}" 107 | Utils.robot.logger.error error 108 | Utils.robot.logger.error error.stack 109 | 110 | module.exports = Webhook 111 | -------------------------------------------------------------------------------- /src/index.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # A hubot script to list and recurrently remind you about open pull requests. 3 | # Optionally receive direct messages when you are assigned to a pull 4 | # request in your organization or for a specific repo or set of repos. 5 | # 6 | # Dependencies: 7 | # - coffeescript 8 | # - cron 9 | # - octokat 10 | # - moment 11 | # - underscore 12 | # - fuse.js 13 | # 14 | # Configuration: 15 | # HUBOT_GITHUB_TOKEN - Github Application Token 16 | # HUBOT_GITHUB_WEBHOOK_SECRET - Optional, if you are using webhooks and have a secret set this for additional security checks on payload delivery 17 | # HUBOT_GITHUB_URL - Set this value if you are using Github Enterprise default: `https://api.github.com` 18 | # HUBOT_GITHUB_ORG - Github Organization Name (the one in the url) 19 | # HUBOT_GITHUB_REPOS_MAP (format: "{"web":["frontend","web"],"android":["android"],"ios":["ios"],"platform":["web"]}" 20 | # 21 | # Commands: 22 | # hubot github open [for ] - Shows a list of open pull requests for the repo of this room [optionally for a specific user] 23 | # hubot github remind hh:mm - I'll remind about open pull requests in this room at hh:mm every weekday. 24 | # hubot github list reminders - See all pull request reminders for this room. 25 | # hubot github reminders in every room - Be nosey and see when other rooms have their reminders set 26 | # hubot github delete hh:mm reminder - If you have a reminder at hh:mm, I'll delete it. 27 | # hubot github delete all reminders - Deletes all reminders for this room. 28 | # 29 | # Author: 30 | # ndaversa 31 | 32 | _ = require 'underscore' 33 | Adapters = require "./adapters" 34 | Config = require "./config" 35 | Github = require "./github" 36 | Reminders = require "./reminders" 37 | Utils = require "./utils" 38 | 39 | class GithubBot 40 | 41 | constructor: (@robot) -> 42 | return new GithubBot @robot unless @ instanceof GithubBot 43 | Utils.robot = @robot 44 | @reminders = new Reminders @robot, "github-reminders", (name) -> 45 | room = Utils.getRoom message: room: name 46 | Github.PullRequests.openForRoom room 47 | @webhook = new Github.Webhook @robot 48 | switch @robot.adapterName 49 | when "slack" 50 | @adapter = new Adapters.Slack @robot 51 | else 52 | @adapter = new Adapters.Generic @robot 53 | 54 | @registerWebhookListeners() 55 | @registerEventListeners() 56 | @registerRobotResponses() 57 | 58 | send: (context, message) -> 59 | @adapter.send context, message 60 | 61 | registerWebhookListeners: -> 62 | disableDisclaimer = """ 63 | If you wish to stop receiving notifications about github reply with: 64 | > github disable notifications 65 | """ 66 | 67 | @robot.on "GithubPullRequestAssigned", (pr, sender) => 68 | @robot.logger.debug "Sending PR assignment notice to #{pr.assignee.name}, sender is #{sender?.name}" 69 | @adapter.dm pr.assignee, 70 | text: """ 71 | You have just been assigned to a pull request #{if sender then "by #{sender.name}" else ""} 72 | """ 73 | author: sender 74 | footer: disableDisclaimer 75 | attachments: [ pr.toAttachment() ] 76 | 77 | @robot.on "GithubPullRequestReviewRequested", (pr, sender) => 78 | @robot.logger.debug "Sending PR review request to #{pr.requested_reviewer.name}, sender is #{sender?.name}" 79 | @adapter.dm pr.requested_reviewer, 80 | text: """ 81 | You have just been requested to review a pull request #{if sender then "by #{sender.name}" else ""} 82 | """ 83 | author: sender 84 | footer: disableDisclaimer 85 | attachments: [ pr.toAttachment() ] 86 | 87 | @robot.on "GithubPullRequestReviewed", (pr, sender) => 88 | @robot.logger.debug "Sending PR reviewed notice to #{pr.user.login}, sender is #{sender?.name}" 89 | @adapter.dm pr.creator, 90 | text: """ 91 | Your pull request has been reviewed by #{if sender then "by #{sender.name}" else ""} 92 | """ 93 | author: sender 94 | footer: disableDisclaimer 95 | attachments: [ pr.toAttachment() ] 96 | 97 | registerEventListeners: -> 98 | @robot.on "GithubPullRequestsOpenForRoom", (prs, room) => 99 | if prs.length is 0 100 | message = text: "No matching pull requests found" 101 | else 102 | attachments = (pr.toAttachment() for pr in prs) 103 | message = attachments: attachments 104 | @send message: room: room.id, message 105 | 106 | registerRobotResponses: -> 107 | 108 | @robot.respond /(?:github|gh|git) (allow|start|enable|disallow|disable|stop)( notifications)?/i, (msg) => 109 | [ __, state ] = msg.match 110 | switch state 111 | when "allow", "start", "enable" 112 | @adapter.enableNotificationsFor msg.message.user 113 | @send msg, """ 114 | Github pull request notifications have been *enabled* 115 | 116 | You will start receiving notifications when you are assigned to a pull request on Github 117 | 118 | If you wish to _disable_ them just send me this message: 119 | > github disable notifications 120 | """ 121 | when "disallow", "stop", "disable" 122 | @adapter.disableNotificationsFor msg.message.user 123 | @send msg, """ 124 | Github pull request notifications have been *disabled* 125 | 126 | You will no longer receive notifications when you are assigned to a pull request on Github 127 | 128 | If you wish to _enable_ them again just send me this message: 129 | > github enable notifications 130 | """ 131 | 132 | @robot.respond /(?:github|gh|git) delete all reminders/i, (msg) => 133 | room = Utils.getRoom msg 134 | remindersCleared = @reminders.clearAllForRoom room.name 135 | @send msg, """ 136 | Deleted #{remindersCleared} reminder#{if remindersCleared is 1 then "" else "s"}. 137 | No more reminders for you. 138 | """ 139 | 140 | @robot.respond /(?:github|gh|git) delete ([0-5]?[0-9]:[0-5]?[0-9]) reminder/i, (msg) => 141 | [__, time] = msg.match 142 | room = Utils.getRoom msg 143 | remindersCleared = @reminders.clearForRoomAtTime room.name, time 144 | if remindersCleared is 0 145 | @send msg, "Nice try. You don't even have a reminder at #{time}" 146 | else 147 | @send msg, "Deleted your #{time} reminder" 148 | 149 | @robot.respond /(?:github|gh|git) remind(?:er)? ((?:[01]?[0-9]|2[0-4]):[0-5]?[0-9])$/i, (msg) => 150 | [__, time] = msg.match 151 | room = Utils.getRoom msg 152 | @reminders.save room.name, time 153 | @send msg, "Ok, from now on I'll remind this room about open pull requests every weekday at #{time}" 154 | 155 | @robot.respond /(?:github|gh|git) list reminders$/i, (msg) => 156 | room = Utils.getRoom msg 157 | reminders = @reminders.getForRoom room.name 158 | if reminders.length is 0 159 | @send msg, "Well this is awkward. You haven't got any github reminders set :-/" 160 | else 161 | @send msg, "You have pull request reminders at the following times: #{_.map(reminders, (reminder) -> reminder.time)}" 162 | 163 | @robot.respond /(?:github|gh|git) reminders in every room/i, (msg) => 164 | reminders = @reminders.getAll() 165 | if reminders.length is 0 166 | @send msg, "No, because there aren't any." 167 | else 168 | @send msg, """ 169 | Here's the reminders for every room: #{_.map(reminders, (reminder) -> "\nRoom: #{reminder.room}, Time: #{reminder.time}")} 170 | """ 171 | 172 | @robot.respond /(github|gh|git) help/i, (msg) => 173 | @send msg, """ 174 | I can remind you about open pull requests for the repo that belongs to this channel 175 | Use me to create a reminder, and then I'll post in this room every weekday at the time you specify. Here's how: 176 | 177 | #{@robot.name} github open [for ] - Shows a list of open pull requests for the repo of this room [optionally for a specific user] 178 | #{@robot.name} github reminder hh:mm - I'll remind about open pull requests in this room at hh:mm every weekday. 179 | #{@robot.name} github list reminders - See all pull request reminders for this room. 180 | #{@robot.name} github reminders in every room - Be nosey and see when other rooms have their reminders set 181 | #{@robot.name} github delete hh:mm reminder - If you have a reminder at hh:mm, I'll delete it. 182 | #{@robot.name} github delete all reminders - Deletes all reminders for this room. 183 | """ 184 | 185 | @robot.respond /(?:github|gh|git) (?:prs|open)(?:\s+(?:for|by)\s+(?:@?)(.*))?/i, (msg) => 186 | [__, who] = msg.match 187 | 188 | if who is 'me' 189 | who = msg.message.user?.name?.toLowerCase() 190 | 191 | if who? 192 | who = @robot.brain.userForName who 193 | who = who.name 194 | 195 | room = Utils.getRoom msg 196 | Github.PullRequests.openForRoom(room, who) 197 | .catch (e) => @send msg, e 198 | 199 | @robot.hear /(?:https?:\/\/github\.com\/([a-z0-9-]+)\/)([a-z0-9-_.]+)\/pull\/(\d+)\/?\s*/i, (msg) => 200 | [ url, org, repo, number ] = msg.match 201 | Github.PullRequest.fromUrl("#{Config.github.url}/repos/#{org}/#{repo}/pulls/#{number}") 202 | .then (pr) => 203 | @robot.emit "JiraFindTicketMatches", "#{pr.title} #{pr.body}", (matches) => 204 | if matches 205 | msg.match = _(matches).unique() 206 | @robot.emit "JiraPrepareResponseForTickets", msg 207 | 208 | module.exports = GithubBot 209 | --------------------------------------------------------------------------------