├── .husky ├── .gitignore ├── pre-push └── pre-commit ├── .gitignore ├── .npmignore ├── script ├── test └── bootstrap ├── .prettierrc ├── .github └── workflows │ ├── nodejs.yml │ └── npm-publish.yml ├── index.js ├── LICENSE ├── package.json ├── CHANGELOG.md ├── src ├── scripts │ ├── pagerduty-webhook.js │ └── pagerduty.js └── pagerduty.js ├── README.md └── test └── pager-me-test.js /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .node-version 3 | .idea 4 | .vscode 5 | .nyc_output 6 | *.tgz -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm test 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .husky 3 | script 4 | test 5 | .release-it.json 6 | CHANGELOG.md 7 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # bootstrap environment 4 | source script/bootstrap 5 | 6 | mocha test/**/*-test.js --reporter spec --exit 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "endOfLine": "lf", 6 | "printWidth": 120, 7 | "trailingComma": "es5", 8 | "singleQuote": true 9 | } 10 | -------------------------------------------------------------------------------- /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" -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | node-version: [16.x, 18.x, 20.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: npm install, build 24 | run: | 25 | npm ci 26 | npm run build --if-present 27 | env: 28 | CI: true 29 | 30 | - name: npm test 31 | run: | 32 | npm test 33 | env: 34 | CI: true 35 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: main 4 | 5 | name: npm-publish 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: "20" 18 | - run: npm ci 19 | - run: npm test 20 | - uses: JS-DevTools/npm-publish@v3 21 | id: publish 22 | with: 23 | token: ${{ secrets.NPM_AUTH_TOKEN }} 24 | - if: ${{ steps.publish.outputs.type }} 25 | name: Create Release 26 | env: 27 | GH_TOKEN: ${{ github.token }} 28 | run: | 29 | VERSION="v${{ steps.publish.outputs.version }}" 30 | gh release create $VERSION --generate-notes 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = function (robot, scripts) { 5 | const scriptsPath = path.resolve(__dirname, 'src', 'scripts'); 6 | return fs.exists(scriptsPath, function (exists) { 7 | if (exists) { 8 | return (() => { 9 | const result = []; 10 | for (var script of Array.from(fs.readdirSync(scriptsPath))) { 11 | if (scripts != null && !Array.from(scripts).includes('*')) { 12 | if (Array.from(scripts).includes(script)) { 13 | result.push(robot.loadFile(scriptsPath, script)); 14 | } else { 15 | result.push(undefined); 16 | } 17 | } else { 18 | result.push(robot.loadFile(scriptsPath, script)); 19 | } 20 | } 21 | return result; 22 | })(); 23 | } 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-pager-me", 3 | "description": "PagerDuty integration for Hubot", 4 | "version": "4.1.0", 5 | "author": "Josh Nichols ", 6 | "license": "MIT", 7 | "keywords": [ 8 | "hubot", 9 | "hubot-scripts", 10 | "pagerduty" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/hubot-scripts/hubot-pager-me.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/hubot-scripts/hubot-pager-me/issues" 18 | }, 19 | "peerDependencies": { 20 | "hubot": ">=3 || 0.0.0-development" 21 | }, 22 | "dependencies": { 23 | "async": "^3.2.4", 24 | "moment-timezone": "^0.5.43", 25 | "nyc": "^15.1.0", 26 | "prettier": "^3.0.2", 27 | "scoped-http-client": "^0.11.0" 28 | }, 29 | "devDependencies": { 30 | "chai": "^4.3.8", 31 | "husky": "^8.0.3", 32 | "mocha": "^10.2.0", 33 | "sinon": "^15.2.0", 34 | "sinon-chai": "^3.7.0" 35 | }, 36 | "main": "index.js", 37 | "scripts": { 38 | "test": "script/test", 39 | "test-with-coverage": "nyc --reporter=text script/test", 40 | "prepare": "husky install", 41 | "format": "prettier --config .prettierrc **/*.js --write" 42 | }, 43 | "files": [ 44 | "src/**/*.js", 45 | "index.js" 46 | ], 47 | "volta": { 48 | "node": "18.19.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2.1.1 2 | ===== 3 | 4 | * check for errors when making HTTP calls, and use hubot's error handling when they do happen 5 | 6 | 2.1.0 7 | ===== 8 | 9 | * fix formatting of Nagios incidents 10 | * add support for listing services and creating maintenance windows 11 | 12 | 13 | 2.0.4 14 | ===== 15 | 16 | * fix `/pager incident` case-sensitivity 17 | 18 | 2.0.3 19 | ===== 20 | 21 | * Update chat response to be less noisy if an incident isn't found 22 | * Add support for restricting which services are displayed through an environment variable 23 | 24 | 2.0.2 25 | ===== 26 | 27 | * Allow `/pager trigger` to work if user has configured PagerDuty email, but it doesn't match a PagerDuty user 28 | 29 | 2.0.1 30 | ===== 31 | 32 | * Allow `/pager trigger` to work if user hasn't configured PagerDuty 33 | * Fix exactly matching an escalation policy 34 | 35 | 2.0.0 36 | ===== 37 | 38 | * Add support for multiple schedules 39 | * HUBOT_PAGERDUTY_SCHEDULE_ID is no longer used. Instead, the schedule name is given as part of the command 40 | * Update `pager trigger` to be assigned to users, escalation policies, or schedules 41 | * Add support for viewing schedules in a given timezone 42 | * Update README with more example interactions 43 | 44 | 1.2.0 45 | ===== 46 | 47 | * Update pager ack and pager resolve to only affect incidents assigned to you 48 | * Add `pager ack!` and `pager resolve!` to preserve older behavior (ie ack and resolve all incidents) 49 | * Add support for nooping interactions in development 50 | * Improve `pager sup` when incident is assigned to multiple users 51 | 52 | 53 | 1.1.0 54 | ===== 55 | 56 | * Add support for showing schedules and overrides, and creating overrides 57 | * Improve error handling when user isn't found in PagerDuty 58 | 59 | 1.0.0 60 | ===== 61 | 62 | * Extract from hubot-scripts repository and converted to script package 63 | -------------------------------------------------------------------------------- /src/scripts/pagerduty-webhook.js: -------------------------------------------------------------------------------- 1 | // Description: 2 | // Receive webhooks from PagerDuty and post them to chat 3 | // 4 | 5 | const pagerRoom = process.env.HUBOT_PAGERDUTY_ROOM; 6 | // Webhook listener endpoint. Set it to whatever URL you want, and make sure it matches your pagerduty service settings 7 | const pagerEndpoint = process.env.HUBOT_PAGERDUTY_ENDPOINT || '/hook'; 8 | 9 | module.exports = function (robot) { 10 | // Webhook listener 11 | let generateIncidentString; 12 | if (pagerEndpoint && pagerRoom) { 13 | robot.router.post(pagerEndpoint, function (req, res) { 14 | robot.messageRoom(pagerRoom, parseWebhook(req, res)); 15 | res.end(); 16 | return; 17 | }); 18 | } 19 | 20 | // Pagerduty Webhook Integration (For a payload example, see http://developer.pagerduty.com/documentation/rest/webhooks) 21 | var parseWebhook = function (req, res) { 22 | const hook = req.body; 23 | 24 | const { messages } = hook; 25 | 26 | if (/^incident.*$/.test(messages[0].type)) { 27 | return parseIncidents(messages); 28 | } else { 29 | return 'No incidents in webhook'; 30 | } 31 | }; 32 | 33 | var parseIncidents = function (messages) { 34 | const returnMessage = []; 35 | let count = 0; 36 | for (var message of Array.from(messages)) { 37 | var { incident } = message.data; 38 | var hookType = message.type; 39 | returnMessage.push(generateIncidentString(incident, hookType)); 40 | count = count + 1; 41 | } 42 | returnMessage.unshift('You have ' + count + ' PagerDuty update(s): \n'); 43 | return returnMessage.join('\n'); 44 | }; 45 | 46 | function getUserForIncident(incident) { 47 | if (incident.assigned_to_user) { 48 | return incident.assigned_to_user.email; 49 | } 50 | if (incident.resolved_by_user) { 51 | return incident.resolved_by_user.email; 52 | } 53 | 54 | return '(???)'; 55 | } 56 | 57 | return (generateIncidentString = function (incident, hookType) { 58 | console.log('hookType is ' + hookType); 59 | const assigned_user = getUserForIncident(incident); 60 | const { incident_number } = incident; 61 | 62 | if (hookType === 'incident.trigger') { 63 | return `\ 64 | Incident # ${incident_number} : 65 | ${incident.status} and assigned to ${assigned_user} 66 | ${incident.html_url} 67 | To acknowledge: @${robot.name} pager me ack ${incident_number} 68 | To resolve: @${robot.name} pager me resolve \ 69 | `; 70 | } else if (hookType === 'incident.acknowledge') { 71 | return `\ 72 | Incident # ${incident_number} : 73 | ${incident.status} and assigned to ${assigned_user} 74 | ${incident.html_url} 75 | To resolve: @${robot.name} pager me resolve ${incident_number}\ 76 | `; 77 | } else if (hookType === 'incident.resolve') { 78 | return `\ 79 | Incident # ${incident_number} has been resolved by ${assigned_user} 80 | ${incident.html_url}\ 81 | `; 82 | } else if (hookType === 'incident.unacknowledge') { 83 | return `\ 84 | ${incident.status} , unacknowledged and assigned to ${assigned_user} 85 | ${incident.html_url} 86 | To acknowledge: @${robot.name} pager me ack ${incident_number} 87 | To resolve: @${robot.name} pager me resolve ${incident_number}\ 88 | `; 89 | } else if (hookType === 'incident.assign') { 90 | return `\ 91 | Incident # ${incident_number} : 92 | ${incident.status} , reassigned to ${assigned_user} 93 | ${incident.html_url} 94 | To resolve: @${robot.name} pager me resolve ${incident_number}\ 95 | `; 96 | } else if (hookType === 'incident.escalate') { 97 | return `\ 98 | Incident # ${incident_number} : 99 | ${incident.status} , was escalated and assigned to ${assigned_user} 100 | ${incident.html_url} 101 | To acknowledge: @${robot.name} pager me ack ${incident_number} 102 | To resolve: @${robot.name} pager me resolve ${incident_number}\ 103 | `; 104 | } 105 | }); 106 | }; 107 | -------------------------------------------------------------------------------- /src/pagerduty.js: -------------------------------------------------------------------------------- 1 | const HttpClient = require('scoped-http-client'); 2 | 3 | const pagerDutyApiKey = process.env.HUBOT_PAGERDUTY_API_KEY; 4 | const pagerDutySubdomain = process.env.HUBOT_PAGERDUTY_SUBDOMAIN; 5 | const pagerDutyBaseUrl = 'https://api.pagerduty.com'; 6 | const pagerDutyServices = process.env.HUBOT_PAGERDUTY_SERVICES; 7 | const pagerDutyFromEmail = process.env.HUBOT_PAGERDUTY_FROM_EMAIL; 8 | let pagerNoop = process.env.HUBOT_PAGERDUTY_NOOP; 9 | if (pagerNoop === 'false' || pagerNoop === 'off') { 10 | pagerNoop = false; 11 | } 12 | 13 | class PagerDutyError extends Error {} 14 | module.exports = { 15 | http(path) { 16 | return HttpClient.create(`${pagerDutyBaseUrl}${path}`).headers({ 17 | Accept: 'application/vnd.pagerduty+json;version=2', 18 | Authorization: `Token token=${pagerDutyApiKey}`, 19 | From: pagerDutyFromEmail, 20 | }); 21 | }, 22 | 23 | missingEnvironmentForApi(msg) { 24 | let missingAnything = false; 25 | if (pagerDutyFromEmail == null) { 26 | msg.send('PagerDuty From is missing: Ensure that HUBOT_PAGERDUTY_FROM_EMAIL is set.'); 27 | missingAnything |= true; 28 | } 29 | if (pagerDutyApiKey == null) { 30 | msg.send('PagerDuty API Key is missing: Ensure that HUBOT_PAGERDUTY_API_KEY is set.'); 31 | missingAnything |= true; 32 | } 33 | return missingAnything; 34 | }, 35 | 36 | get(url, query, cb) { 37 | if (typeof query === 'function') { 38 | cb = query; 39 | query = {}; 40 | } 41 | 42 | if (pagerDutyServices != null && url.match(/\/incidents/)) { 43 | query['service_id'] = pagerDutyServices; 44 | } 45 | 46 | this.http(url).query(query).get()(function (err, res, body) { 47 | if (err != null) { 48 | cb(err); 49 | return; 50 | } 51 | let json_body = null; 52 | switch (res.statusCode) { 53 | case 200: 54 | json_body = JSON.parse(body); 55 | break; 56 | default: 57 | cb(new PagerDutyError(`${res.statusCode} back from ${url}`)); 58 | } 59 | 60 | return cb(null, json_body); 61 | }); 62 | }, 63 | 64 | put(url, data, cb) { 65 | if (pagerNoop) { 66 | console.log(`Would have PUT ${url}: ${inspect(data)}`); 67 | return; 68 | } 69 | 70 | const json = JSON.stringify(data); 71 | this.http(url).header('content-type', 'application/json').put(json)(function (err, res, body) { 72 | if (err != null) { 73 | callback(err); 74 | return; 75 | } 76 | 77 | let json_body = null; 78 | switch (res.statusCode) { 79 | case 200: 80 | json_body = JSON.parse(body); 81 | break; 82 | default: 83 | if (body != null) { 84 | return cb(new PagerDutyError(`${res.statusCode} back from ${url} with body: ${body}`)); 85 | } else { 86 | return cb(new PagerDutyError(`${res.statusCode} back from ${url}`)); 87 | } 88 | } 89 | return cb(null, json_body); 90 | }); 91 | }, 92 | 93 | post(url, data, cb) { 94 | if (pagerNoop) { 95 | console.log(`Would have POST ${url}: ${inspect(data)}`); 96 | return; 97 | } 98 | 99 | const json = JSON.stringify(data); 100 | this.http(url).header('content-type', 'application/json').post(json)(function (err, res, body) { 101 | if (err != null) { 102 | return cb(err); 103 | } 104 | 105 | let json_body = null; 106 | switch (res.statusCode) { 107 | case 201: 108 | json_body = JSON.parse(body); 109 | break; 110 | default: { 111 | cb(new PagerDutyError(`${res.statusCode} back from ${url}`)); 112 | return; 113 | } 114 | } 115 | cb(null, json_body); 116 | }); 117 | }, 118 | 119 | delete(url, cb) { 120 | if (pagerNoop) { 121 | console.log(`Would have DELETE ${url}`); 122 | return; 123 | } 124 | 125 | const auth = `Token token=${pagerDutyApiKey}`; 126 | this.http(url).header('content-length', 0).delete()(function (err, res, body) { 127 | let value; 128 | if (err != null) { 129 | cb(err); 130 | return; 131 | } 132 | const json_body = null; 133 | switch (res.statusCode) { 134 | case 204: 135 | case 200: 136 | value = true; 137 | break; 138 | default: 139 | console.log(res.statusCode); 140 | console.log(body); 141 | value = false; 142 | } 143 | 144 | cb(null, value); 145 | }); 146 | }, 147 | 148 | getIncident(incident_key, cb) { 149 | const query = { incident_key }; 150 | 151 | this.get('/incidents', query, function (err, json) { 152 | if (err != null) { 153 | cb(err); 154 | return; 155 | } 156 | cb(null, json.incidents); 157 | }); 158 | }, 159 | 160 | getIncidents(status, cb) { 161 | const query = { 162 | sort_by: 'incident_number:asc', 163 | 'statuses[]': status.split(','), 164 | }; 165 | 166 | this.get('/incidents', query, function (err, json) { 167 | if (err != null) { 168 | cb(err); 169 | return; 170 | } 171 | 172 | cb(null, json.incidents); 173 | }); 174 | }, 175 | 176 | getSchedules(query, cb) { 177 | if (typeof query === 'function') { 178 | cb = query; 179 | query = {}; 180 | } 181 | 182 | this.get('/schedules', query, function (err, json) { 183 | if (err != null) { 184 | cb(err); 185 | return; 186 | } 187 | 188 | cb(null, json.schedules); 189 | }); 190 | }, 191 | 192 | subdomain: pagerDutySubdomain, 193 | }; 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hubot-pager-me 2 | 3 | [![npm version](https://badge.fury.io/js/hubot-pager-me.svg)](http://badge.fury.io/js/hubot-pager-me) [![Node CI](https://github.com/hubot-scripts/hubot-pager-me/actions/workflows/nodejs.yml/badge.svg)](https://github.com/hubot-scripts/hubot-pager-me/actions/workflows/nodejs.yml) 4 | 5 | PagerDuty integration for Hubot. 6 | 7 | 8 | ## Installation 9 | 10 | In your hubot repository, run: 11 | 12 | `npm install hubot-pager-me --save` 13 | 14 | Then add **hubot-pager-me** to your `external-scripts.json`: 15 | 16 | ```json 17 | ["hubot-pager-me"] 18 | ``` 19 | 20 | ## Configuration 21 | 22 | > **Upgrading from v2.x?** The `HUBOT_PAGERDUTY_SUBDOMAIN` parameter has been replaced with `HUBOT_PAGERDUTY_FROM_EMAIL`, which is [sent along as a header](https://v2.developer.pagerduty.com/docs/rest-api#http-request-headers) to indicate the actor for the incident creation workflow. This would be the email address for a bot user in PagerDuty. 23 | 24 | | Environment Variable | Required? | Description | 25 | | -------------------- | --------- | ----------------------------------------- | 26 | | `HUBOT_PAGERDUTY_API_KEY` | Yes | The [REST API Key](https://support.pagerduty.com/docs/using-the-api#section-generating-an-api-key) for this integration. 27 | | `HUBOT_PAGERDUTY_FROM_EMAIL` | Yes | The email of the default "actor" user for incident creation and modification. | 28 | | `HUBOT_PAGERDUTY_USER_ID` | No`*` | The user ID of a PagerDuty user for your bot. This is only required if you want chat users to be able to trigger incidents without their own PagerDuty user. 29 | | `HUBOT_PAGERDUTY_SERVICE_API_KEY` | No`*` | The [Incident Service Key](https://v2.developer.pagerduty.com/docs/incident-creation-api) to use when creating a new incident. This should be assigned to a dummy escalation policy that doesn't actually notify, as Hubot will trigger on this before reassigning it. 30 | | `HUBOT_PAGERDUTY_SERVICES` | No | Provide a comma separated list of service identifiers (e.g. `PFGPBFY,AFBCGH`) to restrict queries to only those services. 31 | | `HUBOT_PAGERDUTY_SCHEDULES` | No | Provide a comma separated list of schedules identifiers (e.g. `PFGPBFY,AFBCGH`) to restrict queries to only those schedules. 32 | | `HUBOT_PAGERDUTY_DEFAULT_SCHEDULE` | No | A specific schedule that can be triggered using `pager default trigger` to reduce typing for users. 33 | 34 | `*` - May be required for certain actions. 35 | 36 | ### Webhook 37 | 38 | | Environment Variable | Required? | Description | 39 | | -------------------- | --------- | ----------------------------------------- | 40 | | `HUBOT_PAGERDUTY_ENDPOINT` | Yes | PagerDuty webhook listener on your Hubot's server. Must be public. Example: `/hook`. | 41 | | `HUBOT_PAGERDUTY_ROOM` | Yes | Room in which you want the pagerduty webhook notifications to appear. Example: `#pagerduty` | 42 | 43 | To setup the webhooks and get the alerts in your chatrooms, you need to add the endpoint you define here (e.g `/hooks`) in 44 | the service settings of your PagerDuty accounts. You also need to define the room in which you want them to appear. That is, unless you want to spam all the rooms with alerts, but we don't believe that should be the default behavior. 😁 45 | 46 | ## Example interactions 47 | 48 | Trigger an incident assigned to a specific user: 49 | 50 | ``` 51 | technicalpickles> hubot pager trigger jnewland omgwtfbbq 52 | hubot> technicalpickles: :pager: triggered! now assigning it to the right user... 53 | hubot> technicalpickles: :pager: assigned to jnewland! 54 | ``` 55 | 56 | Trigger an incident assigned to an escalation policy: 57 | 58 | ``` 59 | technicalpickles> hubot pager trigger ops site is down 60 | hubot> Shell: :pager: triggered! now assigning it to the right user... 61 | hubot> Shell: :pager: assigned to ops! 62 | ``` 63 | 64 | Check on open incidents: 65 | 66 | ``` 67 | technicalpickles> hubot pager sup 68 | hubot> 69 | Triggered: 70 | ---------- 71 | 8: 2014-11-05T20:17:50Z site is down - @technicalpickles - assigned to jnewland 72 | 73 | Acknowledged: 74 | ------------- 75 | 7: 2014-11-05T20:16:29Z omgwtfbbq - @technicalpickles - assigned to jnewland 76 | ``` 77 | 78 | Acknowledge triggered alerts assigned to you: 79 | 80 | ``` 81 | jnewland> /pager ack 82 | hubot> jnewland: Incident 9 acknowledged 83 | ``` 84 | 85 | Resolve acknowledged alerts assigned to you: 86 | 87 | ``` 88 | jnewland> /pager resolve 89 | hubot> jnewland: Incident 9 resolved 90 | ``` 91 | 92 | Check up coming schedule, and schedule shift overrides on it: 93 | 94 | ``` 95 | technicalpickles> hubot pager schedules 96 | hubot> * Ops - https://urcompany.pagerduty.com/schedules#DEADBEE 97 | technicalpickles> hubot pager schedule ops 98 | hubot> * 2014-06-24T09:06:45-07:00 - 2014-06-25T03:00:00-07:00 technicalpickles 99 | * 2014-06-25T03:00:00-07:00 - 2014-06-26T03:00:00-07:00 jnewland 100 | * 2014-06-26T03:00:00-07:00 - 2014-06-27T03:00:00-07:00 technicalpickles 101 | * 2014-06-27T03:00:00-07:00 - 2014-06-28T03:00:00-07:00 jnewland 102 | * 2014-06-28T03:00:00-07:00 - 2014-06-29T03:00:00-07:00 technicalpickles 103 | technicalpickles> hubot pager override ops 2014-06-25T03:00:00-07:00 - 2014-06-26T03:00:00-07:00 chrislundquist 104 | hubot> Override setup! chrislundquist has the pager from 2014-06-25T06:00:00-04:00 until 2014-06-26T06:00:00-04:00 105 | technicalpickles> hubot pager schedule 106 | hubot> * 2014-06-24T09:06:45-07:00 - 2014-06-25T03:00:00-07:00 technicalpickles 107 | * 2014-06-25T03:00:00-07:00 - 2014-06-26T03:00:00-07:00 chrislundquist 108 | * 2014-06-26T03:00:00-07:00 - 2014-06-27T03:00:00-07:00 technicalpickles 109 | * 2014-06-27T03:00:00-07:00 - 2014-06-28T03:00:00-07:00 jnewland 110 | * 2014-06-28T03:00:00-07:00 - 2014-06-29T03:00:00-07:00 technicalpickles 111 | ``` 112 | 113 | ## Conventions 114 | 115 | `hubot-pager-me` makes some assumptions about how you are using PagerDuty: 116 | 117 | * PagerDuty email matches chat email 118 | * override with `hubot pager me as ` 119 | * The Service used by hubot-pager-me should not be assigned to an escalation policy with real people on it. Instead, it should be a dummy user that doesn't have any notification rules. If this isn't done, the escalation policy assigned to it will be notified, and then Hubot will immediately reassign to the proper team 120 | 121 | ## Development 122 | 123 | Fork this repository, and clone it locally. To start using with an existing Hubot for testing: 124 | 125 | * Run `npm install` in `hubot-pager-me` repository 126 | * Run `npm link` in `hubot-pager-me` repository 127 | * Run `npm link hubot-pager-me` in your Hubot directory 128 | * NOTE: if you are using something like [nodenv](https://github.com/wfarr/nodenv) or similar, make sure your `npm link` from the same node version 129 | 130 | There's a few environment variables useful for testing: 131 | 132 | * `HUBOT_PAGERDUTY_NOOP`: Don't actually make POST/PUT HTTP requests. 133 | * `HUBOT_PAGERDUTY_TEST_EMAIL`: Force email of address to this for testing. 134 | 135 | ## Resources 136 | 137 | * https://v2.developer.pagerduty.com/docs/getting-started 138 | * https://v2.developer.pagerduty.com/docs/webhooks-v2-overview 139 | -------------------------------------------------------------------------------- /test/pager-me-test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const sinon = require('sinon'); 3 | chai.use(require('sinon-chai')); 4 | 5 | const { expect } = chai; 6 | 7 | describe('pagerduty', function () { 8 | before(function () { 9 | this.triggerRegex = 10 | /(pager|major)( me)? (?:trigger|page) ((["'])([^\4]*?)\4|“([^”]*?)”|‘([^’]*?)’|([\.\w\-]+)) ?(.+)?$/i; 11 | this.schedulesRegex = /(pager|major)( me)? schedules( ((["'])([^]*?)\5|(.+)))?$/i; 12 | this.whosOnCallRegex = 13 | /who(?:’s|'s|s| is|se)? (?:on call|oncall|on-call)(?:\?)?(?: (?:for )?((["'])([^]*?)\2|(.*?))(?:\?|$))?$/i; 14 | }); 15 | 16 | beforeEach(function () { 17 | this.robot = { 18 | respond: sinon.spy(), 19 | hear: sinon.spy(), 20 | }; 21 | 22 | require('../src/scripts/pagerduty')(this.robot); 23 | }); 24 | 25 | it('registers a pager me listener', function () { 26 | expect(this.robot.respond).to.have.been.calledWith(/pager( me)?$/i); 27 | }); 28 | 29 | it('registers a pager me as listener', function () { 30 | expect(this.robot.respond).to.have.been.calledWith(/pager(?: me)? as (.*)$/i); 31 | }); 32 | 33 | it('registers a pager forget me listener', function () { 34 | expect(this.robot.respond).to.have.been.calledWith(/pager forget me$/i); 35 | }); 36 | 37 | it('registers a pager incident listener', function () { 38 | expect(this.robot.respond).to.have.been.calledWith(/(pager|major)( me)? incident ([a-z0-9]+)$/i); 39 | }); 40 | 41 | it('registers a pager sup listener', function () { 42 | expect(this.robot.respond).to.have.been.calledWith(/(pager|major)( me)? (inc|incidents|sup|problems)$/i); 43 | }); 44 | 45 | it('registers a pager trigger listener', function () { 46 | expect(this.robot.respond).to.have.been.calledWith(/(pager|major)( me)? (?:trigger|page) ([\w\-]+)$/i); 47 | }); 48 | 49 | it('registers a pager default trigger with message listener', function () { 50 | expect(this.robot.respond).to.have.been.calledWith(/(pager|major)( me)? default (?:trigger|page) ?(.+)?$/i); 51 | }); 52 | 53 | it('registers a pager trigger with message listener', function () { 54 | expect(this.robot.respond).to.have.been.calledWith(this.triggerRegex); 55 | }); 56 | 57 | it('registers a pager ack listener', function () { 58 | expect(this.robot.respond).to.have.been.calledWith(/(?:pager|major)(?: me)? ack(?:nowledge)? (.+)$/i); 59 | }); 60 | 61 | it('registers a pager ack! listener', function () { 62 | expect(this.robot.respond).to.have.been.calledWith(/(pager|major)( me)? ack(nowledge)?(!)?$/i); 63 | }); 64 | 65 | it('registers a pager resolve listener', function () { 66 | expect(this.robot.respond).to.have.been.calledWith(/(?:pager|major)(?: me)? res(?:olve)?(?:d)? (.+)$/i); 67 | }); 68 | 69 | it('registers a pager resolve! listener', function () { 70 | expect(this.robot.respond).to.have.been.calledWith(/(pager|major)( me)? res(olve)?(d)?(!)?$/i); 71 | }); 72 | 73 | it('registers a pager notes on listener', function () { 74 | expect(this.robot.respond).to.have.been.calledWith(/(pager|major)( me)? notes (.+)$/i); 75 | }); 76 | 77 | it('registers a pager notes add listener', function () { 78 | expect(this.robot.respond).to.have.been.calledWith(/(pager|major)( me)? note ([\d\w]+) (.+)$/i); 79 | }); 80 | 81 | it('registers a pager schedules listener', function () { 82 | expect(this.robot.respond).to.have.been.calledWith(this.schedulesRegex); 83 | }); 84 | 85 | it('registers a pager schedule override listener', function () { 86 | expect(this.robot.respond).to.have.been.calledWith( 87 | /(pager|major)( me)? (schedule|overrides)( ((["'])([^]*?)\6|([\w\-]+)))?( ([^ ]+)\s*(\d+)?)?$/i 88 | ); 89 | }); 90 | 91 | it('registers a pager schedule override details listener', function () { 92 | expect(this.robot.respond).to.have.been.calledWith( 93 | /(pager|major)( me)? (override) ((["'])([^]*?)\5|([\w\-]+)) ([\w\-:\+]+) - ([\w\-:\+]+)( (.*))?$/i 94 | ); 95 | }); 96 | 97 | it('registers a pager override delete listener', function () { 98 | expect(this.robot.respond).to.have.been.calledWith( 99 | /(pager|major)( me)? (overrides?) ((["'])([^]*?)\5|([\w\-]+)) (delete) (.*)$/i 100 | ); 101 | }); 102 | 103 | it('registers a pager link listener', function () { 104 | expect(this.robot.respond).to.have.been.calledWith( 105 | /pager( me)? (?!schedules?\b|overrides?\b|my schedule\b)(.+) (\d+)$/i 106 | ); 107 | }); 108 | 109 | it('registers a pager on call listener', function () { 110 | expect(this.robot.respond).to.have.been.calledWith(this.whosOnCallRegex); 111 | }); 112 | 113 | it('registers a pager services listener', function () { 114 | expect(this.robot.respond).to.have.been.calledWith(/(pager|major)( me)? services$/i); 115 | }); 116 | 117 | it('registers a pager maintenance listener', function () { 118 | expect(this.robot.respond).to.have.been.calledWith(/(pager|major)( me)? maintenance (\d+) (.+)$/i); 119 | }); 120 | 121 | it('trigger handles users with dots', function () { 122 | const msg = this.triggerRegex.exec('pager trigger foo.bar baz'); 123 | expect(msg[8]).to.equal('foo.bar'); 124 | expect(msg[9]).to.equal('baz'); 125 | }); 126 | 127 | it('trigger handles users with spaces', function () { 128 | const msg = this.triggerRegex.exec('pager trigger "foo bar" baz'); 129 | expect(msg[5]).to.equal('foo bar'); 130 | expect(msg[9]).to.equal('baz'); 131 | }); 132 | 133 | it('trigger handles users with spaces and single quotes', function () { 134 | const msg = this.triggerRegex.exec("pager trigger 'foo bar' baz"); 135 | expect(msg[5]).to.equal('foo bar'); 136 | expect(msg[9]).to.equal('baz'); 137 | }); 138 | 139 | it('trigger handles users without spaces', function () { 140 | const msg = this.triggerRegex.exec('pager trigger foo bar baz'); 141 | expect(msg[8]).to.equal('foo'); 142 | expect(msg[9]).to.equal('bar baz'); 143 | }); 144 | 145 | it('schedules handles names with quotes', function () { 146 | const msg = this.schedulesRegex.exec('pager schedules "foo bar"'); 147 | expect(msg[6]).to.equal('foo bar'); 148 | }); 149 | 150 | it('schedules handles names without quotes', function () { 151 | const msg = this.schedulesRegex.exec('pager schedules foo bar'); 152 | expect(msg[7]).to.equal('foo bar'); 153 | }); 154 | 155 | it('schedules handles names without spaces', function () { 156 | const msg = this.schedulesRegex.exec('pager schedules foobar'); 157 | expect(msg[7]).to.equal('foobar'); 158 | }); 159 | 160 | it('whos on call handles bad input', function () { 161 | const msg = this.whosOnCallRegex.exec('whos on callllllll'); 162 | expect(msg).to.be.null; 163 | }); 164 | 165 | it('whos on call handles no schedule', function () { 166 | const msg = this.whosOnCallRegex.exec('whos on call'); 167 | expect(msg).to.not.be.null; 168 | }); 169 | 170 | it('whos on call handles schedules with quotes', function () { 171 | const msg = this.whosOnCallRegex.exec('whos on call for "foo bar"'); 172 | expect(msg[3]).to.equal('foo bar'); 173 | }); 174 | 175 | it('whos on call handles schedules with quotes and quesiton mark', function () { 176 | const msg = this.whosOnCallRegex.exec('whos on call for "foo bar"?'); 177 | expect(msg[3]).to.equal('foo bar'); 178 | }); 179 | 180 | it('whos on call handles schedules without quotes', function () { 181 | const msg = this.whosOnCallRegex.exec('whos on call for foo bar'); 182 | expect(msg[4]).to.equal('foo bar'); 183 | }); 184 | 185 | it('whos on call handles schedules without quotes and question mark', function () { 186 | const msg = this.whosOnCallRegex.exec('whos on call for foo bar?'); 187 | expect(msg[4]).to.equal('foo bar'); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /src/scripts/pagerduty.js: -------------------------------------------------------------------------------- 1 | // Description: 2 | // Interact with PagerDuty services, schedules, and incidents with Hubot. 3 | // 4 | // Commands: 5 | // hubot pager me as - remember your pager email is 6 | // hubot pager forget me - forget your pager email 7 | // hubot am I on call - return if I'm currently on call or not 8 | // hubot who's on call - return a list of services and who is on call for them 9 | // hubot who's on call for - return the username of who's on call for any schedule matching 10 | // hubot pager trigger - create a new incident with and assign it to 11 | // hubot pager default trigger - create a new incident with and assign it the user currently on call for our default schedule 12 | // hubot pager trigger - create a new incident with and assign it the user currently on call for 13 | // hubot pager incidents - return the current incidents 14 | // hubot pager sup - return the current incidents 15 | // hubot pager incident - return the incident NNN 16 | // hubot pager note - add note to incident # with 17 | // hubot pager notes - show notes for incident # 18 | // hubot pager problems - return all open incidents 19 | // hubot pager ack - ack incident # 20 | // hubot pager ack - ack triggered incidents assigned to you 21 | // hubot pager ack! - ack all triggered incidents, not just yours 22 | // hubot pager ack ... - ack all specified incidents 23 | // hubot pager resolve - resolve incident # 24 | // hubot pager resolve ... - resolve all specified incidents 25 | // hubot pager resolve - resolve acknowledged incidents assigned to you 26 | // hubot pager resolve! - resolve all acknowledged, not just yours 27 | // hubot pager schedules - list schedules 28 | // hubot pager schedules - list schedules matching 29 | // hubot pager schedule [days] - show 's shifts for the next x [days] (default 30 days) 30 | // hubot pager my schedule - show my on call shifts for the upcoming in all schedules (default 30 days) 31 | // hubot pager me - take the pager for minutes 32 | // hubot pager override - [username] - Create an schedule override from until . If [username] is left off, defaults to you. start and end should date-parsable dates, like 2014-06-24T09:06:45-07:00, see http://momentjs.com/docs/#/parsing/string/ for examples. 33 | // hubot pager overrides [days] - show upcoming overrides for the next x [days] (default 30 days) 34 | // hubot pager override delete - delete an override by its ID 35 | // hubot pager services - list services 36 | // hubot pager maintenance ... - schedule a maintenance window for for specified services 37 | // 38 | // Authors: 39 | // Jesse Newland, Josh Nicols, Jacob Bednarz, Chris Lundquist, Chris Streeter, Joseph Pierri, Greg Hoin, Michael Warkentin 40 | 41 | const pagerduty = require('../pagerduty'); 42 | const async = require('async'); 43 | const { inspect } = require('util'); 44 | const moment = require('moment-timezone'); 45 | 46 | const pagerDutyUserId = process.env.HUBOT_PAGERDUTY_USER_ID; 47 | const pagerDutyServiceApiKey = process.env.HUBOT_PAGERDUTY_SERVICE_API_KEY; 48 | const pagerDutySchedules = process.env.HUBOT_PAGERDUTY_SCHEDULES; 49 | const pagerDutyDefaultSchedule = process.env.HUBOT_PAGERDUTY_DEFAULT_SCHEDULE; 50 | 51 | 52 | module.exports = function (robot) { 53 | let campfireUserToPagerDutyUser; 54 | 55 | robot.respond(/pager( me)?$/i, function (msg) { 56 | if (pagerduty.missingEnvironmentForApi(msg)) { 57 | return; 58 | } 59 | 60 | campfireUserToPagerDutyUser(msg, msg.message.user, function (user) { 61 | const emailNote = (() => { 62 | if (msg.message.user.pagerdutyEmail) { 63 | return `You've told me your PagerDuty email is ${msg.message.user.pagerdutyEmail}`; 64 | } else if (msg.message.user.email_address) { 65 | return `I'm assuming your PagerDuty email is ${msg.message.user.email_address}. Change it with \`${robot.name} pager me as you@yourdomain.com\``; 66 | } 67 | })(); 68 | if (user) { 69 | msg.send(`I found your PagerDuty user ${user.html_url}, ${emailNote}`); 70 | } else { 71 | msg.send(`I couldn't find your user :( ${emailNote}`); 72 | } 73 | }); 74 | 75 | let cmds = robot.helpCommands(); 76 | cmds = (() => { 77 | const result = []; 78 | for (var cmd of Array.from(cmds)) { 79 | if (cmd.match(/hubot (pager |who's on call)/)) { 80 | result.push(cmd); 81 | } 82 | } 83 | return result; 84 | })(); 85 | msg.send(cmds.join('\n')); 86 | }); 87 | 88 | robot.respond(/pager(?: me)? as (.*)$/i, function (msg) { 89 | const email = msg.match[1]; 90 | msg.message.user.pagerdutyEmail = email; 91 | msg.send(`Okay, I'll remember your PagerDuty email is ${email}`); 92 | }); 93 | 94 | robot.respond(/pager forget me$/i, function (msg) { 95 | msg.message.user.pagerdutyEmail = undefined; 96 | msg.send("Okay, I've forgotten your PagerDuty email"); 97 | }); 98 | 99 | robot.respond(/(pager|major)( me)? incident ([a-z0-9]+)$/i, function (msg) { 100 | msg.finish(); 101 | 102 | if (pagerduty.missingEnvironmentForApi(msg)) { 103 | return; 104 | } 105 | 106 | return pagerduty.getIncident(msg.match[3], function (err, incident) { 107 | if (err != null) { 108 | robot.emit('error', err, msg); 109 | return; 110 | } 111 | 112 | if (!incident || !incident['incident']) { 113 | logger.debug(incident); 114 | msg.send('No matching incident found for `msg.match[3]`.'); 115 | return; 116 | } 117 | 118 | msg.send(formatIncident(incident['incident'])); 119 | }); 120 | }); 121 | 122 | robot.respond(/(pager|major)( me)? (inc|incidents|sup|problems)$/i, (msg) => 123 | pagerduty.getIncidents('triggered,acknowledged', function (err, incidents) { 124 | if (err != null) { 125 | robot.emit('error', err, msg); 126 | return; 127 | } 128 | 129 | if (incidents.length == 0) { 130 | msg.send('No open incidents'); 131 | return; 132 | } 133 | 134 | let incident, junk; 135 | let buffer = 'Triggered:\n----------\n'; 136 | const object = incidents.reverse(); 137 | for (junk in object) { 138 | incident = object[junk]; 139 | if (incident.status === 'triggered') { 140 | buffer = buffer + formatIncident(incident); 141 | } 142 | } 143 | buffer = buffer + '\nAcknowledged:\n-------------\n'; 144 | const object1 = incidents.reverse(); 145 | for (junk in object1) { 146 | incident = object1[junk]; 147 | if (incident.status === 'acknowledged') { 148 | buffer = buffer + formatIncident(incident); 149 | } 150 | } 151 | msg.send(buffer); 152 | }) 153 | ); 154 | 155 | robot.respond(/(pager|major)( me)? (?:trigger|page) ([\w\-]+)$/i, (msg) => 156 | msg.reply("Please include a user or schedule to page, like 'hubot pager infrastructure everything is on fire'.") 157 | ); 158 | 159 | robot.respond( 160 | /(pager|major)( me)? default (?:trigger|page) ?(.+)?$/i, 161 | function (msg) { 162 | msg.finish(); 163 | 164 | if (pagerduty.missingEnvironmentForApi(msg)) { 165 | return; 166 | } 167 | if (!pagerDutyDefaultSchedule) { 168 | msg.send("No default schedule configured! Cannot send a page! Please set HUBOT_PAGERDUTY_DEFAULT_SCHEDULE"); 169 | return; 170 | } 171 | const fromUserName = msg.message.user.name; 172 | query = pagerDutyDefaultSchedule; 173 | reason = msg.match[4] || "We Need Help!" 174 | description = `${reason} - @${fromUserName}`; 175 | robot.logger.debug(`Triggering a default page to ${pagerDutyDefaultSchedule} saying ${description}!`); 176 | incidentTrigger(msg, query, description); 177 | } 178 | ); 179 | 180 | robot.respond( 181 | /(pager|major)( me)? (?:trigger|page) ((["'])([^\4]*?)\4|“([^”]*?)”|‘([^’]*?)’|([\.\w\-]+)) ?(.+)?$/i, 182 | function (msg) { 183 | msg.finish(); 184 | 185 | if (pagerduty.missingEnvironmentForApi(msg)) { 186 | return; 187 | } 188 | const fromUserName = msg.message.user.name; 189 | const query = msg.match[5] || msg.match[6] || msg.match[7] || msg.match[8]; 190 | const reason = msg.match[9] || "We Need Help!"; 191 | const description = `${reason} - @${fromUserName}`; 192 | robot.logger.debug(`Triggering a page to ${query} saying ${description}!`); 193 | incidentTrigger(msg, query, description); 194 | } 195 | ); 196 | 197 | robot.respond(/(?:pager|major)(?: me)? ack(?:nowledge)? (.+)$/i, function (msg) { 198 | msg.finish(); 199 | if (pagerduty.missingEnvironmentForApi(msg)) { 200 | return; 201 | } 202 | 203 | const incidentNumbers = parseIncidentNumbers(msg.match[1]); 204 | 205 | // only acknowledge triggered things, since it doesn't make sense to re-acknowledge if it's already in re-acknowledge 206 | // if it ever doesn't need acknowledge again, it means it's timed out and has become 'triggered' again anyways 207 | updateIncidents(msg, incidentNumbers, 'triggered,acknowledged', 'acknowledged'); 208 | }); 209 | 210 | robot.respond(/(pager|major)( me)? ack(nowledge)?(!)?$/i, function (msg) { 211 | if (pagerduty.missingEnvironmentForApi(msg)) { 212 | return; 213 | } 214 | 215 | const force = msg.match[4] != null; 216 | 217 | pagerduty.getIncidents('triggered,acknowledged', function (err, incidents) { 218 | if (err != null) { 219 | robot.emit('error', err, msg); 220 | return; 221 | } 222 | 223 | return campfireUserToPagerDutyUser(msg, msg.message.user, function (user) { 224 | const filteredIncidents = force 225 | ? incidents // don't filter at all 226 | : incidentsByUserId(incidents, user.id); // filter by id 227 | 228 | if (filteredIncidents.length === 0) { 229 | // nothing assigned to the user, but there were others 230 | if (incidents.length > 0 && !force) { 231 | msg.send( 232 | "Nothing assigned to you to acknowledge. Acknowledge someone else's incident with `hubot pager ack `" 233 | ); 234 | } else { 235 | msg.send('Nothing to acknowledge'); 236 | } 237 | return; 238 | } 239 | 240 | const incidentNumbers = Array.from(filteredIncidents).map((incident) => incident.incident_number); 241 | 242 | // only acknowledge triggered things 243 | return updateIncidents(msg, incidentNumbers, 'triggered,acknowledged', 'acknowledged'); 244 | }); 245 | }); 246 | }); 247 | 248 | robot.respond(/(?:pager|major)(?: me)? res(?:olve)?(?:d)? (.+)$/i, function (msg) { 249 | msg.finish(); 250 | 251 | if (pagerduty.missingEnvironmentForApi(msg)) { 252 | return; 253 | } 254 | 255 | const incidentNumbers = parseIncidentNumbers(msg.match[1]); 256 | 257 | // allow resolving of triggered and acknowedlge, since being explicit 258 | return updateIncidents(msg, incidentNumbers, 'triggered,acknowledged', 'resolved'); 259 | }); 260 | 261 | robot.respond(/(pager|major)( me)? res(olve)?(d)?(!)?$/i, function (msg) { 262 | if (pagerduty.missingEnvironmentForApi(msg)) { 263 | return; 264 | } 265 | 266 | const force = msg.match[5] != null; 267 | return pagerduty.getIncidents('acknowledged', function (err, incidents) { 268 | if (err != null) { 269 | robot.emit('error', err, msg); 270 | return; 271 | } 272 | 273 | return campfireUserToPagerDutyUser(msg, msg.message.user, function (user) { 274 | const filteredIncidents = force 275 | ? incidents // don't filter at all 276 | : incidentsByUserId(incidents, user.id); // filter by id 277 | if (filteredIncidents.length === 0) { 278 | // nothing assigned to the user, but there were others 279 | if (incidents.length > 0 && !force) { 280 | msg.send( 281 | "Nothing assigned to you to resolve. Resolve someone else's incident with `hubot pager ack `" 282 | ); 283 | } else { 284 | msg.send('Nothing to resolve'); 285 | } 286 | return; 287 | } 288 | 289 | const incidentNumbers = Array.from(filteredIncidents).map((incident) => incident.incident_number); 290 | 291 | // only resolve things that are acknowledged 292 | return updateIncidents(msg, incidentNumbers, 'acknowledged', 'resolved'); 293 | }); 294 | }); 295 | }); 296 | 297 | robot.respond(/(pager|major)( me)? notes (.+)$/i, function (msg) { 298 | msg.finish(); 299 | 300 | if (pagerduty.missingEnvironmentForApi(msg)) { 301 | return; 302 | } 303 | 304 | const incidentId = msg.match[3]; 305 | pagerduty.get(`/incidents/${incidentId}/notes`, {}, function (err, json) { 306 | if (err != null) { 307 | robot.emit('error', err, msg); 308 | return; 309 | } 310 | 311 | let buffer = ''; 312 | for (var note of Array.from(json.notes)) { 313 | buffer += `${note.created_at} ${note.user.summary}: ${note.content}\n`; 314 | } 315 | msg.send(buffer); 316 | }); 317 | }); 318 | 319 | robot.respond(/(pager|major)( me)? note ([\d\w]+) (.+)$/i, function (msg) { 320 | msg.finish(); 321 | 322 | if (pagerduty.missingEnvironmentForApi(msg)) { 323 | return; 324 | } 325 | 326 | const incidentId = msg.match[3]; 327 | const content = msg.match[4]; 328 | 329 | campfireUserToPagerDutyUser(msg, msg.message.user, function (user) { 330 | const userId = user.id; 331 | if (!userId) { 332 | return; 333 | } 334 | 335 | const data = { 336 | note: { 337 | content, 338 | }, 339 | requester_id: userId, 340 | }; 341 | 342 | pagerduty.post(`/incidents/${incidentId}/notes`, data, function (err, json) { 343 | if (err != null) { 344 | robot.emit('error', err, msg); 345 | return; 346 | } 347 | 348 | if (json && json.note) { 349 | msg.send(`Got it! Note created: ${json.note.content}`); 350 | } else { 351 | msg.send("Sorry, I couldn't do it :("); 352 | } 353 | }); 354 | }); 355 | }); 356 | 357 | robot.respond(/(pager|major)( me)? schedules( ((["'])([^]*?)\5|(.+)))?$/i, function (msg) { 358 | const query = {}; 359 | const scheduleName = msg.match[6] || msg.match[7]; 360 | if (scheduleName) { 361 | query['query'] = scheduleName; 362 | } 363 | 364 | if (pagerduty.missingEnvironmentForApi(msg)) { 365 | return; 366 | } 367 | 368 | pagerduty.getSchedules(query, function (err, schedules) { 369 | if (err != null) { 370 | robot.emit('error', err, msg); 371 | return; 372 | } 373 | 374 | if (schedules.length == 0) { 375 | msg.send('No schedules found!'); 376 | return; 377 | } 378 | 379 | let buffer = ''; 380 | for (var schedule of Array.from(schedules)) { 381 | buffer += `* ${schedule.name} - ${schedule.html_url}\n`; 382 | } 383 | msg.send(buffer); 384 | }); 385 | }); 386 | 387 | robot.respond( 388 | /(pager|major)( me)? (schedule|overrides)( ((["'])([^]*?)\6|([\w\-]+)))?( ([^ ]+)\s*(\d+)?)?$/i, 389 | function (msg) { 390 | let days, timezone; 391 | if (pagerduty.missingEnvironmentForApi(msg)) { 392 | return; 393 | } 394 | 395 | if (msg.match[11]) { 396 | days = msg.match[11]; 397 | } else { 398 | days = 30; 399 | } 400 | 401 | const query = { 402 | since: moment().format(), 403 | until: moment().add(days, 'days').format(), 404 | overflow: 'true', 405 | }; 406 | 407 | let thing = ''; 408 | if (msg.match[3] && msg.match[3].match(/overrides/)) { 409 | thing = 'overrides'; 410 | query['editable'] = 'true'; 411 | } 412 | 413 | const scheduleName = msg.match[7] || msg.match[8]; 414 | 415 | if (!scheduleName) { 416 | msg.reply( 417 | `Please specify a schedule with 'pager ${msg.match[3]} .'' Use 'pager schedules' to list all schedules.` 418 | ); 419 | return; 420 | } 421 | 422 | if (msg.match[10]) { 423 | timezone = msg.match[10]; 424 | } else { 425 | timezone = 'UTC'; 426 | } 427 | 428 | withScheduleMatching(msg, scheduleName, function (schedule) { 429 | const scheduleId = schedule.id; 430 | if (!scheduleId) { 431 | return; 432 | } 433 | 434 | pagerduty.get(`/schedules/${scheduleId}/${thing}`, query, function (err, json) { 435 | if (err != null) { 436 | robot.emit('error', err, msg); 437 | return; 438 | } 439 | 440 | const entries = 441 | __guard__( 442 | __guard__(json != null ? json.schedule : undefined, (x1) => x1.final_schedule), 443 | (x) => x.rendered_schedule_entries 444 | ) || json.overrides; 445 | if (entries) { 446 | const sortedEntries = entries.sort((a, b) => moment(a.start).unix() - moment(b.start).unix()); 447 | 448 | let buffer = ''; 449 | for (var entry of Array.from(sortedEntries)) { 450 | var startTime = moment(entry.start).tz(timezone).format(); 451 | var endTime = moment(entry.end).tz(timezone).format(); 452 | if (entry.id) { 453 | buffer += `* (${entry.id}) ${startTime} - ${endTime} ${entry.user.summary}\n`; 454 | } else { 455 | buffer += `* ${startTime} - ${endTime} ${entry.user.name}\n`; 456 | } 457 | } 458 | if (buffer === '') { 459 | msg.send('None found!'); 460 | } else { 461 | msg.send(buffer); 462 | } 463 | } else { 464 | msg.send('None found!'); 465 | } 466 | }); 467 | }); 468 | } 469 | ); 470 | 471 | robot.respond(/(pager|major)( me)? my schedule( ([^ ]+)\s?(\d+))?$/i, function (msg) { 472 | let days; 473 | if (pagerduty.missingEnvironmentForApi(msg)) { 474 | return; 475 | } 476 | 477 | if (msg.match[5]) { 478 | days = msg.match[5]; 479 | } else { 480 | days = 30; 481 | } 482 | 483 | campfireUserToPagerDutyUser(msg, msg.message.user, function (user) { 484 | let timezone; 485 | const userId = user.id; 486 | 487 | const query = { 488 | since: moment().format(), 489 | until: moment().add(days, 'days').format(), 490 | overflow: 'true', 491 | }; 492 | 493 | if (msg.match[4]) { 494 | timezone = msg.match[4]; 495 | } else { 496 | timezone = 'UTC'; 497 | } 498 | 499 | pagerduty.getSchedules(function (err, schedules) { 500 | if (err != null) { 501 | robot.emit('error', err, msg); 502 | return; 503 | } 504 | 505 | if (schedules.length > 0) { 506 | const renderSchedule = (schedule, cb) => 507 | pagerduty.get(`/schedules/${schedule.id}`, query, function (err, json) { 508 | if (err != null) { 509 | cb(err); 510 | } 511 | 512 | const entries = __guard__( 513 | __guard__(json != null ? json.schedule : undefined, (x1) => x1.final_schedule), 514 | (x) => x.rendered_schedule_entries 515 | ); 516 | 517 | if (entries) { 518 | const sortedEntries = entries.sort((a, b) => moment(a.start).unix() - moment(b.start).unix()); 519 | 520 | let buffer = ''; 521 | for (var entry of Array.from(sortedEntries)) { 522 | if (userId === entry.user.id) { 523 | var startTime = moment(entry.start).tz(timezone).format(); 524 | var endTime = moment(entry.end).tz(timezone).format(); 525 | 526 | buffer += `* ${startTime} - ${endTime} ${entry.user.summary} (${schedule.name})\n`; 527 | } 528 | } 529 | cb(null, buffer); 530 | } 531 | }); 532 | 533 | return async.map(schedules, renderSchedule, function (err, results) { 534 | if (err != null) { 535 | robot.emit('error', err, msg); 536 | return; 537 | } 538 | msg.send(results.join('')); 539 | }); 540 | } else { 541 | msg.send('No schedules found!'); 542 | } 543 | }); 544 | }); 545 | }); 546 | 547 | robot.respond( 548 | /(pager|major)( me)? (override) ((["'])([^]*?)\5|([\w\-]+)) ([\w\-:\+]+) - ([\w\-:\+]+)( (.*))?$/i, 549 | function (msg) { 550 | let overrideUser; 551 | if (pagerduty.missingEnvironmentForApi(msg)) { 552 | return; 553 | } 554 | 555 | const scheduleName = msg.match[6] || msg.match[7]; 556 | 557 | if (msg.match[11]) { 558 | overrideUser = robot.brain.userForName(msg.match[11]); 559 | 560 | if (!overrideUser) { 561 | msg.send("Sorry, I don't seem to know who that is. Are you sure they are in chat?"); 562 | return; 563 | } 564 | } else { 565 | overrideUser = msg.message.user; 566 | } 567 | 568 | campfireUserToPagerDutyUser(msg, overrideUser, function (user) { 569 | const userId = user.id; 570 | if (!userId) { 571 | return; 572 | } 573 | 574 | withScheduleMatching(msg, scheduleName, function (schedule) { 575 | const scheduleId = schedule.id; 576 | if (!scheduleId) { 577 | return; 578 | } 579 | 580 | if (moment(msg.match[8]).isValid() && moment(msg.match[9]).isValid()) { 581 | const start_time = moment(msg.match[8]).format(); 582 | const end_time = moment(msg.match[9]).format(); 583 | 584 | const override = { 585 | start: start_time, 586 | end: end_time, 587 | user: { 588 | id: userId, 589 | type: 'user_reference', 590 | }, 591 | }; 592 | const data = { override }; 593 | return pagerduty.post(`/schedules/${scheduleId}/overrides`, data, function (err, json) { 594 | if (err != null) { 595 | robot.emit('error', err, msg); 596 | return; 597 | } 598 | 599 | if (json && json.override) { 600 | const start = moment(json.override.start); 601 | const end = moment(json.override.end); 602 | msg.send( 603 | `Override setup! ${ 604 | json.override.user.summary 605 | } has the pager from ${start.format()} until ${end.format()}` 606 | ); 607 | } else { 608 | msg.send("That didn't work. Check Hubot's logs for an error!"); 609 | } 610 | }); 611 | } else { 612 | msg.send('Please use a http://momentjs.com/ compatible date!'); 613 | } 614 | }); 615 | }); 616 | } 617 | ); 618 | 619 | robot.respond(/(pager|major)( me)? (overrides?) ((["'])([^]*?)\5|([\w\-]+)) (delete) (.*)$/i, function (msg) { 620 | if (pagerduty.missingEnvironmentForApi(msg)) { 621 | return; 622 | } 623 | 624 | const scheduleName = msg.match[6] || msg.match[7]; 625 | 626 | withScheduleMatching(msg, scheduleName, function (schedule) { 627 | const scheduleId = schedule.id; 628 | if (!scheduleId) { 629 | return; 630 | } 631 | 632 | pagerduty.delete(`/schedules/${scheduleId}/overrides/${msg.match[9]}`, function (err, success) { 633 | if (success) { 634 | msg.send(':boom:'); 635 | } else { 636 | msg.send('Something went weird.'); 637 | } 638 | }); 639 | }); 640 | }); 641 | 642 | robot.respond(/pager( me)? (?!schedules?\b|overrides?\b|my schedule\b)(.+) (\d+)$/i, function (msg) { 643 | msg.finish(); 644 | 645 | if (pagerduty.missingEnvironmentForApi(msg)) { 646 | return; 647 | } 648 | 649 | campfireUserToPagerDutyUser(msg, msg.message.user, function (user) { 650 | const userId = user.id; 651 | if (!userId) { 652 | return; 653 | } 654 | 655 | if (!msg.match[2] || msg.match[2] === 'me') { 656 | msg.reply( 657 | "Please specify a schedule with 'pager me infrastructure 60'. Use 'pager schedules' to list all schedules." 658 | ); 659 | return; 660 | } 661 | const schedule = msg.match[2].replace(/(^"|"$)/mg, ''); 662 | const minutes = parseInt(msg.match[3]); 663 | withScheduleMatching(msg, schedule, function (matchingSchedule) { 664 | if (!matchingSchedule.id) { 665 | return; 666 | } 667 | 668 | let start = moment().format(); 669 | let end = moment().add(minutes, 'minutes').format(); 670 | const override = { 671 | start, 672 | end, 673 | user: { 674 | id: userId, 675 | type: 'user_reference', 676 | }, 677 | }; 678 | withCurrentOncall(msg, matchingSchedule, function (old_username, schedule) { 679 | const data = { override: override }; 680 | return pagerduty.post(`/schedules/${schedule.id}/overrides`, data, function (err, json) { 681 | if (err != null) { 682 | robot.emit('error', err, msg); 683 | return; 684 | } 685 | 686 | if (json.override) { 687 | start = moment(json.override.start); 688 | end = moment(json.override.end); 689 | msg.send( 690 | `Rejoice, ${old_username}! ${json.override.user.summary} has the pager on ${ 691 | schedule.name 692 | } until ${end.format()}` 693 | ); 694 | } 695 | }); 696 | }); 697 | }); 698 | }); 699 | }); 700 | 701 | robot.respond(/am i on (call|oncall|on-call)/i, function (msg) { 702 | if (pagerduty.missingEnvironmentForApi(msg)) { 703 | return; 704 | } 705 | 706 | campfireUserToPagerDutyUser(msg, msg.message.user, function (user) { 707 | const userId = user.id; 708 | 709 | const renderSchedule = (s, cb) => 710 | withCurrentOncallId(msg, s, function (oncallUserid, oncallUsername, schedule) { 711 | if (userId === oncallUserid) { 712 | cb(null, `* Yes, you are on call for ${schedule.name} - ${schedule.html_url}`); 713 | } else if (oncallUsername === null) { 714 | cb(null, `* No, you are NOT on call for ${schedule.name} - ${schedule.html_url}`); 715 | } else { 716 | cb( 717 | null, 718 | `* No, you are NOT on call for ${schedule.name} (but ${oncallUsername} is) - ${schedule.html_url}` 719 | ); 720 | } 721 | }); 722 | 723 | if (userId == null) { 724 | msg.send("Couldn't figure out the pagerduty user connected to your account."); 725 | } else { 726 | pagerduty.getSchedules(function (err, schedules) { 727 | if (err != null) { 728 | robot.emit('error', err, msg); 729 | return; 730 | } 731 | 732 | if (schedules.length == 0) { 733 | msg.send('No schedules found!'); 734 | } 735 | 736 | async.map(schedules, renderSchedule, function (err, results) { 737 | if (err != null) { 738 | robot.emit('error', err, msg); 739 | return; 740 | } 741 | msg.send(results.join('\n')); 742 | }); 743 | }); 744 | } 745 | }); 746 | }); 747 | 748 | // who is on call? 749 | robot.respond( 750 | /who(?:’s|'s|s| is|se)? (?:on call|oncall|on-call)(?:\?)?(?: (?:for )?((["'])([^]*?)\2|(.*?))(?:\?|$))?$/i, 751 | function (msg) { 752 | if (pagerduty.missingEnvironmentForApi(msg)) { 753 | return; 754 | } 755 | 756 | const scheduleName = msg.match[3] || msg.match[4]; 757 | 758 | const messages = []; 759 | let allowed_schedules = []; 760 | if (pagerDutySchedules != null) { 761 | allowed_schedules = pagerDutySchedules.split(','); 762 | } 763 | 764 | const renderSchedule = (s, cb) => 765 | withCurrentOncall(msg, s, function (username, schedule) { 766 | // If there is an allowed schedules array, skip returned schedule not in it 767 | if (allowed_schedules.length && !Array.from(allowed_schedules).includes(schedule.id)) { 768 | robot.logger.debug(`Schedule ${schedule.id} (${schedule.name}) not in HUBOT_PAGERDUTY_SCHEDULES`); 769 | cb(null); 770 | return; 771 | } 772 | 773 | // Ignore schedule if no user assigned to it 774 | if (username) { 775 | messages.push(`* ${username} is on call for ${schedule.name} - ${schedule.html_url}`); 776 | } else { 777 | robot.logger.debug(`No user for schedule ${schedule.name}`); 778 | } 779 | 780 | // Return callback 781 | cb(null); 782 | }); 783 | 784 | if (scheduleName != null) { 785 | SchedulesMatching(msg, scheduleName, (s) => 786 | async.map(s, renderSchedule, function (err) { 787 | if (err != null) { 788 | robot.emit('error', err, msg); 789 | return; 790 | } 791 | msg.send(messages.join('\n')); 792 | }) 793 | ); 794 | } else { 795 | pagerduty.getSchedules(function (err, schedules) { 796 | if (err != null) { 797 | robot.emit('error', err, msg); 798 | return; 799 | } 800 | if (schedules.length == 0) { 801 | msg.send('No schedules found!'); 802 | return; 803 | } 804 | 805 | async.map(schedules, renderSchedule, function (err) { 806 | if (err != null) { 807 | robot.emit('error', err, msg); 808 | return; 809 | } 810 | msg.send(messages.join('\n')); 811 | }); 812 | }); 813 | } 814 | } 815 | ); 816 | 817 | robot.respond(/(pager|major)( me)? services$/i, function (msg) { 818 | if (pagerduty.missingEnvironmentForApi(msg)) { 819 | return; 820 | } 821 | 822 | return pagerduty.get('/services', {}, function (err, json) { 823 | if (err != null) { 824 | robot.emit('error', err, msg); 825 | return; 826 | } 827 | 828 | let buffer = ''; 829 | const { services } = json; 830 | if (services.length == 0) { 831 | msg.send('No services found!'); 832 | return; 833 | } 834 | 835 | for (var service of Array.from(services)) { 836 | buffer += `* ${service.id}: ${service.name} (${service.status}) - ${service.html_url}\n`; 837 | } 838 | msg.send(buffer); 839 | }); 840 | }); 841 | 842 | robot.respond(/(pager|major)( me)? maintenance (\d+) (.+)$/i, function (msg) { 843 | if (pagerduty.missingEnvironmentForApi(msg)) { 844 | return; 845 | } 846 | 847 | return campfireUserToPagerDutyUser(msg, msg.message.user, function (user) { 848 | const requester_id = user.id; 849 | if (!requester_id) { 850 | return; 851 | } 852 | 853 | const minutes = msg.match[3]; 854 | const service_ids = msg.match[4].split(' '); 855 | const start_time = moment().format(); 856 | const end_time = moment().add('minutes', minutes).format(); 857 | 858 | const services = []; 859 | for (var service_id of Array.from(service_ids)) { 860 | services.push({ id: service_id, type: 'service_reference' }); 861 | } 862 | 863 | const maintenance_window = { start_time, end_time, services }; 864 | const data = { maintenance_window, services }; 865 | 866 | msg.send(`Opening maintenance window for: ${service_ids}`); 867 | pagerduty.post('/maintenance_windows', data, function (err, json) { 868 | if (err != null) { 869 | robot.emit('error', err, msg); 870 | return; 871 | } 872 | 873 | if (json && json.maintenance_window) { 874 | msg.send( 875 | `Maintenance window created! ID: ${json.maintenance_window.id} Ends: ${json.maintenance_window.end_time}` 876 | ); 877 | return; 878 | } 879 | 880 | msg.send("That didn't work. Check Hubot's logs for an error!"); 881 | }); 882 | }); 883 | }); 884 | 885 | var parseIncidentNumbers = (match) => match.split(/[ ,]+/).map((incidentNumber) => parseInt(incidentNumber)); 886 | 887 | var reassignmentParametersForUserOrScheduleOrEscalationPolicy = function (msg, string, cb) { 888 | let campfireUser; 889 | if ((campfireUser = robot.brain.userForName(string))) { 890 | return campfireUserToPagerDutyUser(msg, campfireUser, (user) => 891 | cb({ assigned_to_user: user.id, name: user.name }) 892 | ); 893 | } else { 894 | return pagerduty.get('/escalation_policies', { query: string }, function (err, json) { 895 | if (err != null) { 896 | robot.emit('error', err, msg); 897 | return; 898 | } 899 | 900 | let escalationPolicy = null; 901 | 902 | if (__guard__(json != null ? json.escalation_policies : undefined, (x) => x.length) === 1) { 903 | escalationPolicy = json.escalation_policies[0]; 904 | // Multiple results returned and one is exact (case-insensitive) 905 | } else if (__guard__(json != null ? json.escalation_policies : undefined, (x1) => x1.length) > 1) { 906 | const matchingExactly = json.escalation_policies.filter( 907 | (es) => es.name.toLowerCase() === string.toLowerCase() 908 | ); 909 | if (matchingExactly.length === 1) { 910 | escalationPolicy = matchingExactly[0]; 911 | } 912 | } 913 | 914 | if (escalationPolicy != null) { 915 | return cb({ escalation_policy: escalationPolicy.id, name: escalationPolicy.name }); 916 | } else { 917 | return SchedulesMatching(msg, string, function (schedule) { 918 | if (schedule) { 919 | withCurrentOncallUser(msg, schedule, (user, schedule) => 920 | cb({ assigned_to_user: user.id, name: user.name }) 921 | ); 922 | } else { 923 | cb(); 924 | } 925 | }); 926 | } 927 | }); 928 | } 929 | }; 930 | 931 | var pagerDutyIntegrationAPI = function (msg, cmd, description, cb) { 932 | if (pagerDutyServiceApiKey == null) { 933 | msg.send('PagerDuty API service key is missing.'); 934 | msg.send('Ensure that HUBOT_PAGERDUTY_SERVICE_API_KEY is set.'); 935 | return; 936 | } 937 | 938 | let data = null; 939 | switch (cmd) { 940 | case 'trigger': 941 | data = JSON.stringify({ service_key: pagerDutyServiceApiKey, event_type: 'trigger', description }); 942 | pagerDutyIntegrationPost(msg, data, (json) => cb(json)); 943 | } 944 | }; 945 | 946 | var formatIncident = function (inc) { 947 | let assigned_to; 948 | const summary = inc.title; 949 | const assignee = __guard__( 950 | __guard__(inc.assignments != null ? inc.assignments[0] : undefined, (x1) => x1['assignee']), 951 | (x) => x['summary'] 952 | ); 953 | if (assignee) { 954 | assigned_to = `- assigned to ${assignee}`; 955 | } else { 956 | (''); 957 | } 958 | return `${inc.incident_number}: ${inc.created_at} ${summary} ${assigned_to}\n`; 959 | }; 960 | 961 | var updateIncidents = (msg, incidentNumbers, statusFilter, updatedStatus) => 962 | campfireUserToPagerDutyUser(msg, msg.message.user, function (user) { 963 | const requesterId = user.id; 964 | if (!requesterId) { 965 | return; 966 | } 967 | 968 | return pagerduty.getIncidents(statusFilter, function (err, incidents) { 969 | let incident; 970 | if (err != null) { 971 | robot.emit('error', err, msg); 972 | return; 973 | } 974 | 975 | const foundIncidents = []; 976 | for (incident of Array.from(incidents)) { 977 | // FIXME this isn't working very consistently 978 | if (incidentNumbers.indexOf(incident.incident_number) > -1) { 979 | foundIncidents.push(incident); 980 | } 981 | } 982 | 983 | if (foundIncidents.length === 0) { 984 | return msg.reply( 985 | `Couldn't find incident(s) ${incidentNumbers.join(', ')}. Use \`${ 986 | robot.name 987 | } pager incidents\` for listing.` 988 | ); 989 | } else { 990 | const data = { 991 | incidents: foundIncidents.map((incident) => ({ 992 | id: incident.id, 993 | type: 'incident_reference', 994 | status: updatedStatus, 995 | })), 996 | }; 997 | 998 | return pagerduty.put('/incidents', data, function (err, json) { 999 | if (err != null) { 1000 | robot.emit('error', err, msg); 1001 | return; 1002 | } 1003 | 1004 | if (json != null ? json.incidents : undefined) { 1005 | let buffer = 'Incident'; 1006 | if (json.incidents.length > 1) { 1007 | buffer += 's'; 1008 | } 1009 | buffer += ' '; 1010 | buffer += (() => { 1011 | const result = []; 1012 | for (incident of Array.from(json.incidents)) { 1013 | result.push(incident.incident_number); 1014 | } 1015 | return result; 1016 | })().join(', '); 1017 | buffer += ` ${updatedStatus}`; 1018 | return msg.reply(buffer); 1019 | } else { 1020 | return msg.reply(`Problem updating incidents ${incidentNumbers.join(',')}`); 1021 | } 1022 | }); 1023 | } 1024 | }); 1025 | }); 1026 | 1027 | var pagerDutyIntegrationPost = (msg, json, cb) => 1028 | msg 1029 | .http('https://events.pagerduty.com/generic/2010-04-15/create_event.json') 1030 | .header('content-type', 'application/json') 1031 | .post(json)(function (err, res, body) { 1032 | switch (res.statusCode) { 1033 | case 200: 1034 | json = JSON.parse(body); 1035 | return cb(json); 1036 | default: 1037 | console.log(res.statusCode); 1038 | return console.log(body); 1039 | } 1040 | }); 1041 | 1042 | var incidentsByUserId = (incidents, userId) => 1043 | incidents.filter(function (incident) { 1044 | const assignments = incident.assignments.map((item) => item.assignee.id); 1045 | return assignments.some((assignment) => assignment === userId); 1046 | }); 1047 | 1048 | var withCurrentOncall = (msg, schedule, cb) => 1049 | withCurrentOncallUser(msg, schedule, function (user, s) { 1050 | if (user) { 1051 | return cb(user.name, s); 1052 | } else { 1053 | return cb(null, s); 1054 | } 1055 | }); 1056 | 1057 | var withCurrentOncallId = (msg, schedule, cb) => 1058 | withCurrentOncallUser(msg, schedule, function (user, s) { 1059 | if (user) { 1060 | return cb(user.id, user.name, s); 1061 | } else { 1062 | return cb(null, null, s); 1063 | } 1064 | }); 1065 | 1066 | var withCurrentOncallUser = function (msg, schedule, cb) { 1067 | const oneHour = moment().add(1, 'hours').format(); 1068 | const now = moment().format(); 1069 | 1070 | let scheduleId = schedule.id; 1071 | if (schedule instanceof Array && schedule[0]) { 1072 | scheduleId = schedule[0].id; 1073 | } 1074 | if (!scheduleId) { 1075 | msg.send("Unable to retrieve the schedule. Use 'pager schedules' to list all schedules."); 1076 | return; 1077 | } 1078 | 1079 | const query = { 1080 | since: now, 1081 | until: oneHour, 1082 | }; 1083 | return pagerduty.get(`/schedules/${scheduleId}/users`, query, function (err, json) { 1084 | if (err != null) { 1085 | robot.emit('error', err, msg); 1086 | return; 1087 | } 1088 | if (json.users && json.users.length > 0) { 1089 | return cb(json.users[0], schedule); 1090 | } else { 1091 | return cb(null, schedule); 1092 | } 1093 | }); 1094 | }; 1095 | 1096 | var SchedulesMatching = function (msg, q, cb) { 1097 | const query = { 1098 | query: q, 1099 | }; 1100 | return pagerduty.getSchedules(query, function (err, schedules) { 1101 | if (err != null) { 1102 | robot.emit('error', err, msg); 1103 | return; 1104 | } 1105 | 1106 | return cb(schedules); 1107 | }); 1108 | }; 1109 | 1110 | var withScheduleMatching = (msg, q, cb) => 1111 | SchedulesMatching(msg, q, function (schedules) { 1112 | if ((schedules != null ? schedules.length : undefined) < 1) { 1113 | msg.send(`I couldn't find any schedules matching ${q}`); 1114 | } else { 1115 | for (var schedule of Array.from(schedules)) { 1116 | cb(schedule); 1117 | } 1118 | } 1119 | }); 1120 | 1121 | var incidentTrigger = (msg, query, description) => 1122 | // Figure out who we are 1123 | campfireUserToPagerDutyUser(msg, msg.message.user, false, function (triggerdByPagerDutyUser) { 1124 | const triggerdByPagerDutyUserId = (() => { 1125 | if (triggerdByPagerDutyUser != null) { 1126 | return triggerdByPagerDutyUser.id; 1127 | } else if (pagerDutyUserId) { 1128 | return pagerDutyUserId; 1129 | } 1130 | })(); 1131 | if (!triggerdByPagerDutyUserId) { 1132 | msg.send( 1133 | `Sorry, I can't figure your PagerDuty account, and I don't have my own :( Can you tell me your PagerDuty email with \`${robot.name} pager me as you@yourdomain.com\` or make sure you've set the HUBOT_PAGERDUTY_USER_ID environment variable?` 1134 | ); 1135 | return; 1136 | } 1137 | 1138 | // Figure out what we're trying to page 1139 | reassignmentParametersForUserOrScheduleOrEscalationPolicy(msg, query, function (results) { 1140 | if (!(results.assigned_to_user || results.escalation_policy)) { 1141 | msg.reply(`Couldn't find a user or unique schedule or escalation policy matching ${query} :/`); 1142 | return; 1143 | } 1144 | 1145 | return pagerDutyIntegrationAPI(msg, 'trigger', description, function (json) { 1146 | query = { incident_key: json.incident_key }; 1147 | 1148 | msg.reply(':pager: triggered! now assigning it to the right user...'); 1149 | 1150 | return setTimeout( 1151 | () => 1152 | pagerduty.get('/incidents', query, function (err, json) { 1153 | if (err != null) { 1154 | robot.emit('error', err, msg); 1155 | return; 1156 | } 1157 | 1158 | if ((json != null ? json.incidents.length : undefined) === 0) { 1159 | msg.reply("Couldn't find the incident we just created to reassign. Please try again :/"); 1160 | } else { 1161 | } 1162 | 1163 | let data = null; 1164 | if (results.escalation_policy) { 1165 | data = { 1166 | incidents: json.incidents.map((incident) => ({ 1167 | id: incident.id, 1168 | type: 'incident_reference', 1169 | 1170 | escalation_policy: { 1171 | id: results.escalation_policy, 1172 | type: 'escalation_policy_reference', 1173 | }, 1174 | })), 1175 | }; 1176 | } else { 1177 | data = { 1178 | incidents: json.incidents.map((incident) => ({ 1179 | id: incident.id, 1180 | type: 'incident_reference', 1181 | 1182 | assignments: [ 1183 | { 1184 | assignee: { 1185 | id: results.assigned_to_user, 1186 | type: 'user_reference', 1187 | }, 1188 | }, 1189 | ], 1190 | })), 1191 | }; 1192 | } 1193 | 1194 | return pagerduty.put('/incidents', data, function (err, json) { 1195 | if (err != null) { 1196 | robot.emit('error', err, msg); 1197 | return; 1198 | } 1199 | 1200 | if ((json != null ? json.incidents.length : undefined) === 1) { 1201 | return msg.reply(`:pager: assigned to ${results.name}!`); 1202 | } else { 1203 | return msg.reply('Problem reassigning the incident :/'); 1204 | } 1205 | }); 1206 | }), 1207 | 10000 1208 | ); 1209 | }); 1210 | }); 1211 | }); 1212 | 1213 | const userEmail = (user) => 1214 | user.pagerdutyEmail || 1215 | user.email_address || 1216 | (user.profile != null ? user.profile.email : undefined) || 1217 | process.env.HUBOT_PAGERDUTY_TEST_EMAIL; 1218 | 1219 | return (campfireUserToPagerDutyUser = function (msg, user, required, cb) { 1220 | if (typeof required === 'function') { 1221 | cb = required; 1222 | required = true; 1223 | } 1224 | 1225 | //# Determine the email based on the adapter type (v4.0.0+ of the Slack adapter stores it in `profile.email`) 1226 | const email = userEmail(user); 1227 | const speakerEmail = userEmail(msg.message.user); 1228 | 1229 | if (!email) { 1230 | if (!required) { 1231 | cb(null); 1232 | return; 1233 | } else { 1234 | const possessive = email === speakerEmail ? 'your' : `${user.name}'s`; 1235 | const addressee = email === speakerEmail ? 'you' : `${user.name}`; 1236 | 1237 | msg.send( 1238 | `Sorry, I can't figure out ${possessive} email address :( Can ${addressee} tell me with \`${robot.name} pager me as you@yourdomain.com\`?` 1239 | ); 1240 | return; 1241 | } 1242 | } 1243 | 1244 | pagerduty.get('/users', { query: email }, function (err, json) { 1245 | if (err != null) { 1246 | robot.emit('error', err, msg); 1247 | return; 1248 | } 1249 | 1250 | if (json.users.length !== 1) { 1251 | if (json.users.length === 0 && !required) { 1252 | cb(null); 1253 | return; 1254 | } else { 1255 | msg.send( 1256 | `Sorry, I expected to get 1 user back for ${email}, but got ${json.users.length} :sweat:. If your PagerDuty email is not ${email} use \`/pager me as ${email}\`` 1257 | ); 1258 | return; 1259 | } 1260 | } 1261 | 1262 | cb(json.users[0]); 1263 | }); 1264 | }); 1265 | }; 1266 | 1267 | function __guard__(value, transform) { 1268 | return typeof value !== 'undefined' && value !== null ? transform(value) : undefined; 1269 | } 1270 | --------------------------------------------------------------------------------