UNPKG

hubot-scripts

Version:

Allows you to opt in to a variety of scripts

376 lines (325 loc) 13.6 kB
# Description: # Octospy GitHub events, watch what's happening with your projects # Powered by http://developer.github.com/v3/repos/hooks/ # # Dependencies: # "underscore": "1.3.3" # "handlebars": "1.0.5beta" # # Configuration: # HUBOT_URL # HUBOT_GITHUB_USER # HUBOT_GITHUB_PASSWORD # or # HUBOT_GITHUB_TOKEN # # Commands: # hubot octospy <repo> [event_type] - Start watching events for the repo, default push # hubot octospy stop <repo> [event_type] - Stop watching events for the repo # hubot octospying - Show what you're spying on # hubot octospy events - List the events you can watch # # Author: # rcs _ = require 'underscore' QS = require 'querystring' Handlebars = require 'handlebars' Handlebars.registerHelper 'trim', (str, length) -> str.substring 0, length Handlebars.registerHelper 'overflow', (str, length) -> if str.length > length str.substring(0,length-3) + '...' else str # Internal: Given a template name and a context, return the compiled template. # Returns JSONed context if no template is found. # # event - The event type we're rendering # context - The object to give the template # # If the template in the views hash is a function, pass it the context to get the specific template renderTemplate = (event,context) -> if views[event] if _.isFunction(views[event]) message = views[event](context) else template = Handlebars.compile(views[event]) message = template(context) else # We couldn't find a template, so let's push this out. People on github like JSON, right? message = {} message[event] = req.body message = JSON.stringify message return message # Private: Helper method for pubsub modification. # # msg - The hubot msg object, used for its http client # action - The action to take, 'subscribe' or 'unsubscribe' # target - The hash containing the subscription we want to work on # github_url - The base github URL # repo - The repository # event - The event type # cb - Function to pass as a callback to the HTTP call # # Example: # # pubsub_modify(msg, 'subscribe', { github_url: 'github.com', repo: 'github/hubot', event: 'push' }, (err,resp,body) -> msg.send "aaaaaallllright.") # pubsub_modify = (msg, action, target, cb) -> {github_url, repo, event} = target data = QS.stringify { "hub.mode": action, "hub.topic": "https://#{github_url}/#{repo}/events/#{event}.json" "hub.callback": "#{process.env.HUBOT_URL}/hubot/octospy/#{github_url}/#{event}" } # Check authentication, return error if it isn't specified if process.env.HUBOT_GITHUB_TOKEN auth = "token #{process.env.HUBOT_GITHUB_TOKEN}" else if (process.env.HUBOT_GITHUB_USER and process.env.HUBOT_GITHUB_PASSWORD) auth = 'Basic ' + new Buffer("#{process.env.HUBOT_GITHUB_USER}:#{process.env.HUBOT_GITHUB_PASSWORD}").toString('base64') else return cb({},{statusCode: 401}, {message: "Octospy doesn't have credentials"}) msg.http("https://api.#{github_url}") .path('/hub') .header('Authorization', auth) .post(data) cb # These are views for each of the event types. # Note: Handlebars likes to HTML escape things. It's kinda lame as a default. {{{ }}} to avoid it. views = push: (context) -> if context.created template = """ {{pusher.name}} created a new branch "{{branch}}" on {{repo_name}} {{compare}} """ else if context.commits.length > 3 context.extra_commits = context.commits.length - 3 context.short_commits = context.commits.slice(0,3) template = """ {{pusher.name}} pushed to {{branch}} at {{repo_name}} {{compare}} {{#each short_commits}} {{author.username}}: {{trim id 7}} {{{overflow message 80}}} {{/each}}{{#if extra_commits }} ... +{{extra_commits}} more{{/if}} """ template = Handlebars.compile(template) message = template(context) issues: """ {{sender.login}} {{action}} issue {{issue.number}} on {{repo_name}} "{{{overflow issue.title 25}}}" {{issue.html_url}} """ issue_comment: """ {{sender.login}} commented on issue {{issue.number}} on {{repo_name}} "{{{overflow issue.title 25}}}" {{issue.html_url}} > {{{overflow comment.body 120}}} """ commit_comment: """ {{sender.login}} commented on commit {{comment.commit_id}} on {{repo_name}} {{comment.html_url}} > {{{overflow comment.body 120}}} """ pull_request: (context) -> template = switch context.action when 'opened' """ {{sender.login}} {{action}} pull request {{number}} on {{repo_name}}: "{{{overflow pull_request.title 25}}}" {{pull_request.html_url}} {{pull_request.commits}} commits with {{pull_request.additions}} additions and {{pull_request.deletions}} deletions """ when 'closed' switch context.pull_request.merged when true """ {{sender.login}} merged pull request {{number}} on {{repo_name}}: "{{{overflow pull_request.title 25}}}" {{pull_request.html_url}} """ else """ {{sender.login}} closed pull request {{number}} on {{repo_name}} without merging: "{{{overflow pull_request.title 25}}}" {{pull_request.html_url}} """ when 'synchronize' """ {{sender.login}} updated pull request {{number}} on {{repo_name}}: "{{{overflow pull_request.title 25}}}" {{pull_request.html_url}} """ template = Handlebars.compile(template) message = template(context) pull_request_review_comment: """ {{sender.login}} commented on pull request {{pull_request.number}} on {{repo_name}} "{{{overflow pull_request.title 25}}}" {{pull_request.html_url}} > {{{overflow comment.body 120}}} """ gollum: """ {{#each pages}} {{../sender.login}} {{action}} wiki page on {{repo_name}}: "{{{overflow title 25}}}" {{html_url}} {{/each}} """ watch: """ {{sender.login}} started watching {{repo_name}} http://{{github_url}}/{{sender.login}} """ download: """ {{sender.login}} added a download to {{repo_name}}: "{{{overflow download.name 25}}}" {{download.html_url}} """ fork: """ {{sender.login}} forked {{repo_name}} {{forkee.html_url}} """ fork_apply: """ {{sender.login}} merged from the fork queue to {{head}} on {{repo_name}} """ member: """ {{sender.login}} added {{member.login}} as a collaborator to {{repo_name}} """ public: """ {{sender.login}} turned {{repo_name}} public """ module.exports = (robot) -> # Internal: Initialize our brain robot.brain.on 'loaded', => robot.brain.data.octospy ||= {} # Public: Announce the kinds of things octospy knows about robot.respond /octospy events/i, (msg) -> msg.reply "I know about " + ( event for event of views ).join(', ') # Public: Dump the watching hash robot.respond /octospying/i, (msg) -> watching = [] # Troll octospy's data for any possible listeners, then see if they're us for github_url, github of robot.brain.data.octospy for repo_name, repo of github for event, listeners of repo if _.include(listeners, msg.message.user.id) watching.push github_url: github_url repo_name: repo_name event: event if watching.length > 0 msg.reply (for sub in watching "#{sub.repo_name} #{sub.event} events" + if sub.github_url != 'github.com' " on #{sub.github_url}" else "" ) else msg.reply "I don't think you're octospying anything" # Public: Unsubscribe from an event type for a repository # # repo - The repository name (ex. 'github/hubot' # event - The event type to stop watching (default: 'push') # github_url - The base github URL (default: 'github.com' robot.respond /octospy stop ([^ ]+\/[^ ]+) ?([^ ]*)? ?([^ ]*)?/i, (msg) -> repo = msg.match[1] event = msg.match[2] || 'push' github_url = msg.match[3] || 'github.com' # Convenience accessor listeners = robot.brain.data.octospy[github_url]?[repo]?[event] if ! listeners return msg.send "Can't find any octospies for #{repo} #{event} events" # Find the user in possible listeners for listener, i in listeners when listener == msg.message.user.id removed = listeners.splice(i,1) # Didn't find the user if ! removed return msg.reply "I don't think you're octospying #{repo} #{event} events" else msg.reply "Unoctospied #{repo} #{event} events on #{github_url}" # We're done if nobody's left return unless listeners.length == 0 # Otherwise we unsub pubsub_modify msg, 'unsubscribe', { github_url: github_url, repo: repo, event: event }, (err,res,body) -> switch res.statusCode when 204 data = robot.brain.data.octospy repos = data[github_url] events = repos[repo] # Clean up after ourselves delete events[event] delete repos[repo] if (a for a of events).length == 0 delete data[github_url] if (a for a of repos).length == 0 # Here to hook the redis magic # robot.brain.data.octospy = data robot.logger.info "The last user unsubscribed. Removed my subscription to #{repo} #{event} events" else robot.logger.warning "Failed to unsubscribe to #{repo} #{event} events on #{github_url}: #{body} (Status Code: #{res.statusCode})" # Public: Subsribe to an event type for a repository # # repo - The repository name (ex. 'github/hubot' # event - The event type to stop watching (default: 'push') # github_url - The base github URL (default: 'github.com' robot.respond /octospy ([^ ]+\/[^ ]+) ?([^ ]*)? ?([^ ]*)?/i, (msg) -> repo = msg.match[1] event = msg.match[2] || 'push' github_url = msg.match[3] || 'github.com' # Don't go any further if we don't know about the event type if ! _.include(( known for known of views ), event) return msg.reply "Sorry, I don't know about #{event} events" # Convenience accessor listeners = robot.brain.data.octospy[github_url]?[repo]?[event] # Internal: Add a listener # # Closes around msg, repo, event, github_url addListener = -> # Vivify! repos = robot.brain.data.octospy[github_url] ||= {} events = repos[repo] ||= {} listeners = events[event] ||= [] # See whether we're already listening if (listener for listener in listeners when listener == msg.message.user.id).length == 0 listeners.push msg.message.user.id msg.reply "Octospying #{repo} #{event} events on #{github_url}" else msg.reply "You're already octospying that." # Check to see if we have any subscriptions to this event type for the # repo, and if not, register the subscription if ! listeners pubsub_modify msg, 'subscribe', { github_url: github_url, repo: repo, event: event }, (err,res,body) -> switch res.statusCode when 204 addListener() when 401 msg.reply """ Failed to auth: #{JSON.stringify body} Specify credentials in the environment. (HUBOT_GITHUB_USERNAME,HUBOT_GITHUB_PASSWORD) or HUBOT_GITHUB_TOKEN" To create a token: curl -u 'user:pass' https://api.github.com/authorizations -d '{"scopes":["repo"],"note":"Hubot Octospy"}' """ when 422 msg.reply "Either #{repo} doesn't exist, or my credentials don't make me a collaborator on it. Couldn't subscribe." robot.logger.info "#{JSON.stringify body}" else msg.reply "I failed to subscribe to #{repo} #{event} events on #{github_url}: #{body} (Status Code: #{res.statusCode})" robot.logger.warning "#{JSON.stringify body}" else addListener() # Public: Repond to POSTs from github # # :github - The github base url we registered, so we know the source of this POST # :event - The event type that was registered robot.router.post '/hubot/octospy/:github/:event', (req, res) -> req.body = req.body || {} return res.end "ok" unless req.body.repository # Not something we care about. Who does this? # Convenience accessors event = req.params.event repo_name = (req.body.repository.owner.login || req.body.repository.owner.name) + "/" + req.body.repository.name github_url = req.params.github # Extend the context for our templates context = _.extend req.body, repo: req.body.repository repo_name: repo_name github_url: github_url branch: if req.body.ref req.body.ref.replace(/^refs\/heads\//,'') else undefined message = '[octospy] ' + renderTemplate(event,context) # Tell the people who care listeners = (robot.brain.userForId(id) for id in (robot.brain.data.octospy[github_url]?[repo_name][event] || [])) # group rooms together, so we don't spam with multiple people with subs for room, users of _.groupBy(listeners, 'room') when room robot.send users[0], message # For users without rooms, send individually for room, users of _.groupBy(listeners, 'room') when ! room robot.send(user, message) for user in users res.writeHead(204) res.end()