├── .editorconfig ├── .env.dist ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Caddyfile ├── LICENSE.md ├── Makefile ├── README-hubot.md ├── README.md ├── RELEASE.md ├── bin ├── create-docker-stack.php ├── get-latest-tag.php ├── hubot └── hubot.cmd ├── docker-compose.yml ├── docker-stack.yml.dist ├── etc ├── docker │ ├── caddy.Dockerfile │ ├── hubot.Dockerfile │ └── nginx.Dockerfile └── nginx │ ├── nginx.conf │ └── nginx.dev.conf ├── external-scripts.json ├── img └── zf-logo.png ├── lib ├── discourse-categories.coffee ├── discourse-category.coffee ├── discourse-post.coffee ├── discourse-topic.coffee ├── discourse-verify-signature.coffee ├── docs-build.coffee ├── error-handler.coffee ├── github-issue-comment.coffee ├── github-issues.coffee ├── github-pull-request-review-comment.coffee ├── github-pull-request-review.coffee ├── github-pull-request.coffee ├── github-push.coffee ├── github-release.coffee ├── github-status.coffee ├── twitter-stream.coffee ├── twitter-tweeter.coffee ├── twitter-tweetstream.coffee ├── twitter-types.coffee └── zf-acl.coffee ├── package-lock.json ├── package.json ├── scripts ├── catch-all.coffee ├── discourse.coffee ├── docs.coffee ├── error-handler.coffee ├── github.coffee ├── tweetstream.coffee └── zf-acls.coffee └── var └── www └── index.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | LC_ALL=C.UTF-8 2 | LANG=C.UTF-8 3 | PORT=9001 4 | HUBOT_ADAPTER=slack 5 | HUBOT_NAME=zf-bot 6 | REDIS_URL=redis://redis:6379/hubot 7 | HUBOT_SLACK_BOTNAME=zf-bot 8 | HUBOT_SLACK_TOKEN= 9 | HUBOT_SLACK_TEAM= 10 | HUBOT_TWITTER_CONSUMER_KEY= 11 | HUBOT_TWITTER_CONSUMER_SECRET= 12 | HUBOT_TWITTER_ACCESS_TOKEN_KEY= 13 | HUBOT_TWITTER_ACCESS_TOKEN_SECRET= 14 | HUBOT_DISCOURSE_URL=https://discourse.zendframework.com 15 | HUBOT_DISCOURSE_SECRET= 16 | HUBOT_GITHUB_TOKEN= 17 | HUBOT_GITHUB_CALLBACK_URL_BASE= 18 | HUBOT_GITHUB_CALLBACK_SECRET= 19 | HUBOT_GITHUB_DEFAULT_ORG=zendframework 20 | HUBOT_GITHUB_RELEASE_CALLBACK= 21 | HUBOT_ZF_ACL_USER_WHITELIST= 22 | HUBOT_ERROR_ROOM=server-errors 23 | HUBOT_DOCS_GITHUB_USER= 24 | HUBOT_DOCS_GITHUB_EMAIL= 25 | HUBOT_DOCS_GITHUB_TOKEN= 26 | HUBOT_DOCS_API_TOKEN= 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store* 2 | .env 3 | .env.sh 4 | .hubot_history 5 | docker-compose.override.yml 6 | docker-stack.yml 7 | etc/docker/docker-compose.dummy.yml 8 | etc/docker/nginx-dummy.Dockerfile 9 | etc/nginx/certs/ 10 | etc/nginx/nginx.dummy.conf 11 | node_modules/ 12 | scripts/test.coffee 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 0.7.0 - 2017-09-27 6 | 7 | ### Added 8 | 9 | - Nothing. 10 | 11 | ### Changed 12 | 13 | - [#6](https://github.com/zendframework/zfbot/pull/6) updates the hubot 14 | container to use nodejs v8 + npm v5, eliminating the need for yarn. 15 | 16 | ### Deprecated 17 | 18 | - Nothing. 19 | 20 | ### Removed 21 | 22 | - Nothing. 23 | 24 | ### Fixed 25 | 26 | - Nothing. 27 | 28 | ## 0.6.4 - 2017-08-14 29 | 30 | ### Added 31 | 32 | - Nothing. 33 | 34 | ### Changed 35 | 36 | - Updates how the issue webhook displays the event originator; instead of the 37 | issue user (who is always the reporter), it now uses the event sender. 38 | 39 | ### Deprecated 40 | 41 | - Nothing. 42 | 43 | ### Removed 44 | 45 | - Nothing. 46 | 47 | ### Fixed 48 | 49 | - Nothing. 50 | 51 | ## 0.6.3 - 2017-08-14 52 | 53 | ### Added 54 | 55 | - Nothing. 56 | 57 | ### Changed 58 | 59 | - Updates how the pull-request webhook displays the event originator; instead of the 60 | pull-request user (who is always the reporter), it now uses the event sender. 61 | 62 | ### Deprecated 63 | 64 | - Nothing. 65 | 66 | ### Removed 67 | 68 | - Nothing. 69 | 70 | ### Fixed 71 | 72 | - Nothing. 73 | 74 | ## 0.6.2 - 2017-08-08 75 | 76 | ### Added 77 | 78 | - Nothing. 79 | 80 | ### Changed 81 | 82 | - Updates the `github list` command to sort the repositories during display. 83 | 84 | ### Deprecated 85 | 86 | - Nothing. 87 | 88 | ### Removed 89 | 90 | - Nothing. 91 | 92 | ### Fixed 93 | 94 | - Fixes 95 | 96 | ## 0.6.1 - 2017-08-03 97 | 98 | ### Added 99 | 100 | - [#3](https://github.com/zendframework/zfbot/pull/3) adds gulp to the hubot 101 | container. 102 | 103 | ### Changed 104 | 105 | - [#3](https://github.com/zendframework/zfbot/pull/3) removes installation 106 | instructions for `python3` from the hubot image, as it's already present in the 107 | base container. 108 | 109 | ### Deprecated 110 | 111 | - Nothing. 112 | 113 | ### Removed 114 | 115 | - Nothing. 116 | 117 | ### Fixed 118 | 119 | - Nothing. 120 | 121 | ## 0.6.0 - 2017-07-26 122 | 123 | ### Added 124 | 125 | - [#2](https://github.com/zendframework/zfbot/pull/2) adds listener middleware 126 | within `scripts/zf-acls.coffee` for verifying ACLs. 127 | 128 | ### Changed 129 | 130 | - [#2](https://github.com/zendframework/zfbot/pull/2) modifies each of 131 | `lib/twitter-tweetstream.coffee`, `lib/zf-acl.coffee`, `scripts/docs.coffee`, 132 | `scripts/github.coffee`, `scripts/tweetstream.coffee`, and 133 | `scripts/zf-acls.coffee` to pass the listener metadata `id: "authorize"` when 134 | matching text to which to respond; this then triggers the new authorization 135 | listener middleware. 136 | 137 | ### Deprecated 138 | 139 | - Nothing. 140 | 141 | ### Removed 142 | 143 | - [#2](https://github.com/zendframework/zfbot/pull/2) removes the 144 | `lib/authorize.coffee` module, as it is now redundant. 145 | 146 | ### Fixed 147 | 148 | - [#2](https://github.com/zendframework/zfbot/pull/2) fixes how users are 149 | removed from the authorization whitelist, switching from `_.remove` to 150 | `_.pull`, as the latter returns the modified array. 151 | - [#2](https://github.com/zendframework/zfbot/pull/2) fixes how users are 152 | restored during initialization to ensure no duplicates occur. 153 | 154 | 155 | ## 0.5.1 - 2017-07-26 156 | 157 | ### Added 158 | 159 | - zfbot now responds to mentions of the form `{org}/{repo}#\d+` with a link to 160 | the issue or pull request. 161 | 162 | ### Deprecated 163 | 164 | - Nothing. 165 | 166 | ### Removed 167 | 168 | - Nothing. 169 | 170 | ### Fixed 171 | 172 | - Nothing. 173 | 174 | ## 0.5.0 - 2017-07-26 175 | 176 | ### Added 177 | 178 | - [#1](https://github.com/zendframework/zfbot/pull/1) adds new modules: 179 | - `discourse-categories` returns an array of category details for known 180 | Discourse categories in the ZF instance. 181 | - `discourse-category` provides a function for looking up a Discourse category 182 | by identifier. 183 | - `discourse-post` handles an incoming "post" webhook event from Discourse. 184 | - `discourse-topic` handles an incoming "topic" webhook event from Discourse. 185 | - `discourse-verify-signature` performs hmac-sha256 signature verification for 186 | Discourse webhook payloads. 187 | 188 | ### Changed 189 | 190 | - [#1](https://github.com/zendframework/zfbot/pull/1) changes the `discourse` 191 | script such that it no longer exposes any commands to Slack, and instead 192 | registers routes for handling incoming Discourse webhooks. 193 | 194 | ### Deprecated 195 | 196 | - Nothing. 197 | 198 | ### Removed 199 | 200 | - [#1](https://github.com/zendframework/zfbot/pull/1) removes the 201 | `discourse-listener` module, in favor of using webhooks. 202 | 203 | ### Fixed 204 | 205 | - Nothing. 206 | 207 | ## 0.4.2 - 2017-07-20 208 | 209 | ### Added 210 | 211 | - Nothing. 212 | 213 | ### Deprecated 214 | 215 | - Nothing. 216 | 217 | ### Removed 218 | 219 | - Nothing. 220 | 221 | ### Fixed 222 | 223 | - Fixes variable interpolations in the Discourse listener when reporting an error. 224 | 225 | ## 0.4.1 - 2017-07-19 226 | 227 | ### Added 228 | 229 | - Nothing. 230 | 231 | ### Deprecated 232 | 233 | - Nothing. 234 | 235 | ### Removed 236 | 237 | - Nothing. 238 | 239 | ### Fixed 240 | 241 | - Removes duplicate posts from Discourse watches within a channel. 242 | 243 | ## 0.4.0 - 2017-07-18 244 | 245 | ### Added 246 | 247 | - Adds functionality for building ZF documentation via two mechanisms: 248 | 249 | - Calling ` docs build ` from within a channel (as a whitelisted user) 250 | - Via a new "build-success" event, emitted by the github-status handler when a 251 | succcessful build against a master branch occurs. 252 | 253 | The functionality requires several new dependencies in the container, as well 254 | as new environment variables to allow deploying the documentation. 255 | 256 | ### Deprecated 257 | 258 | - Nothing. 259 | 260 | ### Removed 261 | 262 | - Nothing. 263 | 264 | ### Fixed 265 | 266 | - Nothing. 267 | 268 | 269 | ## 0.3.4 - 2017-07-17 270 | 271 | ### Added 272 | 273 | - Nothing. 274 | 275 | ### Deprecated 276 | 277 | - Nothing. 278 | 279 | ### Removed 280 | 281 | - Nothing. 282 | 283 | ### Fixed 284 | 285 | - Fixes error handling when unable to parse JSON returned when fetching topics 286 | from Discourse. 287 | 288 | ## 0.3.3 - 2017-07-17 289 | 290 | ### Added 291 | 292 | - Adds logging of rejected releases from the GitHub release callback handler. 293 | 294 | ### Deprecated 295 | 296 | - Nothing. 297 | 298 | ### Removed 299 | 300 | - Nothing. 301 | 302 | ### Fixed 303 | 304 | - Nothing. 305 | 306 | ## 0.3.2 - 2017-07-17 307 | 308 | ### Added 309 | 310 | - Nothing. 311 | 312 | ### Deprecated 313 | 314 | - Nothing. 315 | 316 | ### Removed 317 | 318 | - Nothing. 319 | 320 | ### Fixed 321 | 322 | - Fixes a typo when importing the crypto library within the github-release 323 | handler. 324 | 325 | ## 0.3.1 - 2017-07-17 326 | 327 | ### Added 328 | 329 | - Adds the ability to notify a configured callback URL with GitHub release 330 | details once processing of the release is complete. 331 | 332 | ### Deprecated 333 | 334 | - Nothing. 335 | 336 | ### Removed 337 | 338 | - Nothing. 339 | 340 | ### Fixed 341 | 342 | - Nothing. 343 | 344 | ## 0.3.0 - 2017-07-11 345 | 346 | ### Added 347 | 348 | - Adds the following commands (both behind authorization): 349 | - `zfbot tweet ` 350 | - `zfbot retweet ` 351 | 352 | ### Deprecated 353 | 354 | - Nothing. 355 | 356 | ### Removed 357 | 358 | - Nothing. 359 | 360 | ### Fixed 361 | 362 | - Nothing. 363 | 364 | ## 0.2.2 - 2017-07-11 365 | 366 | ### Added 367 | 368 | - Nothing. 369 | 370 | ### Deprecated 371 | 372 | - Nothing. 373 | 374 | ### Removed 375 | 376 | - Nothing. 377 | 378 | ### Fixed 379 | 380 | - Fixes an issue with the catch-all when a message contains no text. 381 | 382 | ## 0.2.1 - 2017-07-10 383 | 384 | ### Added 385 | 386 | - Nothing. 387 | 388 | ### Deprecated 389 | 390 | - Nothing. 391 | 392 | ### Removed 393 | 394 | - Nothing. 395 | 396 | ### Fixed 397 | 398 | - Catches `JSON.parse` errors in the Discourse plugin. 399 | 400 | ## 0.2.0 - 2017-07-06 401 | 402 | ### Added 403 | 404 | - The `tweetstream` plugin now exposes a `tweet` event on the robot instance, 405 | allowing other plugins to send tweets. 406 | - The github-release integration now sends a tweet via the `tweet` event. 407 | 408 | ### Deprecated 409 | 410 | - Nothing. 411 | 412 | ### Removed 413 | 414 | - Nothing. 415 | 416 | ### Fixed 417 | 418 | - Nothing. 419 | 420 | ## 0.1.0 - 2017-07-05 421 | 422 | Initial (stable?) release of zfbot. 423 | 424 | ### Added 425 | 426 | - ACL system; only people in the ACL whitelist can perform privileged actions. 427 | - Twitter: follow users or track searches, per room. 428 | - Discourse: follow Discourse categories for the configured Discourse instance, per room. 429 | - GitHub: subscribe via PubSubHubbub to GitHub events for a given repository, per room. Currently knows about: 430 | - issue creation and closure. 431 | - issue comments. 432 | - pull request creation, merging, and closure. 433 | - pull request comments (these are issue comments). 434 | - pull request reviews and comments. 435 | - releases. 436 | - status updates (for success, failure, and error states). 437 | 438 | ### Deprecated 439 | 440 | - Nothing. 441 | 442 | ### Removed 443 | 444 | - Nothing. 445 | 446 | ### Fixed 447 | 448 | - Nothing. 449 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | This project adheres to [The Code Manifesto](http://codemanifesto.com) 4 | as its guidelines for contributor interactions. 5 | 6 | ## The Code Manifesto 7 | 8 | We want to work in an ecosystem that empowers developers to reach their 9 | potential — one that encourages growth and effective collaboration. A space that 10 | is safe for all. 11 | 12 | A space such as this benefits everyone that participates in it. It encourages 13 | new developers to enter our field. It is through discussion and collaboration 14 | that we grow, and through growth that we improve. 15 | 16 | In the effort to create such a place, we hold to these values: 17 | 18 | 1. **Discrimination limits us.** This includes discrimination on the basis of 19 | race, gender, sexual orientation, gender identity, age, nationality, technology 20 | and any other arbitrary exclusion of a group of people. 21 | 2. **Boundaries honor us.** Your comfort levels are not everyone’s comfort 22 | levels. Remember that, and if brought to your attention, heed it. 23 | 3. **We are our biggest assets.** None of us were born masters of our trade. 24 | Each of us has been helped along the way. Return that favor, when and where 25 | you can. 26 | 4. **We are resources for the future.** As an extension of #3, share what you 27 | know. Make yourself a resource to help those that come after you. 28 | 5. **Respect defines us.** Treat others as you wish to be treated. Make your 29 | discussions, criticisms and debates from a position of respectfulness. Ask 30 | yourself, is it true? Is it necessary? Is it constructive? Anything less is 31 | unacceptable. 32 | 6. **Reactions require grace.** Angry responses are valid, but abusive language 33 | and vindictive actions are toxic. When something happens that offends you, 34 | handle it assertively, but be respectful. Escalate reasonably, and try to 35 | allow the offender an opportunity to explain themselves, and possibly correct 36 | the issue. 37 | 7. **Opinions are just that: opinions.** Each and every one of us, due to our 38 | background and upbringing, have varying opinions. The fact of the matter, is 39 | that is perfectly acceptable. Remember this: if you respect your own 40 | opinions, you should respect the opinions of others. 41 | 8. **To err is human.** You might not intend it, but mistakes do happen and 42 | contribute to build experience. Tolerate honest mistakes, and don't hesitate 43 | to apologize if you make one yourself. 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | Everyone is welcome to contribute to zf-bot. Contributing doesn’t just mean 4 | submitting pull requests; there are many different ways for you to get involved, 5 | including answering questions in chat, reporting or triaging issues, and 6 | documenting the bot. 7 | 8 | ## Conduct 9 | 10 | No matter how you want to get involved, we ask that you first learn what’s 11 | expected of anyone who participates in the project by reading the [Code 12 | Manifesto](CODE_OF_CONDUCT.md). By participating, you are expected to follow its 13 | guidelines and contribute positively within the community. 14 | 15 | ## Pull Requests 16 | 17 | We love pull requests. Here's a quick guide: 18 | 19 | - Check for [existing issues](../issues) for duplicates and confirm that it hasn't 20 | been fixed already in the [master branch](../). 21 | - Fork the repo, and clone it locally. 22 | - Create a new branch for your contribution. 23 | - Add tests if you are able to. Currently, we have no tests, so proposing a 24 | testing framework and initial tests would be an excellent way to contribute! 25 | - Provide a fix if you are capable. 26 | - Push to your fork and submit a pull request. 27 | 28 | At this point you're waiting on us. We may suggest some changes or improvements 29 | or alternatives. Please be aware that this is one of close to 200 repositories 30 | we maintain, so we may not be able to review your issues and pull requests for a 31 | matter of days or weeks. 32 | 33 | If you're adding a new feature, it should not conflict with existing features; 34 | conflicts will need to be resolved and/or justified so we may roll out the new 35 | feature. 36 | 37 | If you are proposing a change to a user-facing API, we will need a 38 | justification: does it simplify usage? does it provide forwards compatibility 39 | with another proposed feature? does it make usage more flexible, allowing new 40 | features in the future? etc. 41 | 42 | Some things that will increase the chance that your pull request is accepted: 43 | 44 | - Make sure the tests pass (once we have some!). 45 | - Update the documentation: code comments, example code, guides. Basically, 46 | update everything affected by your contribution. 47 | - Include any information that would be relevant to reproducing bugs, use cases 48 | for new features, etc. 49 | - Your commits are associated with your GitHub user: https://help.github.com/articles/why-are-my-commits-linked-to-the-wrong-user/ 50 | - Make pull requests against the correct branch. Bugfixes should be submitted 51 | against master, new features against develop. 52 | 53 | ### Stale issue and pull request policy 54 | 55 | Issues and pull requests have a shelf life and sometimes they are no longer 56 | relevant. All issues and pull requests that have not had any activity for 180 57 | days will be marked as stale. Simply leave a comment with information about why 58 | it may still be relevant to keep it open. If no activity occurs in the next 7 59 | days, we will close it. 60 | 61 | The goal of this process is to keep the list of open issues and pull requests 62 | focused on work that is actionable and important for the maintainers and the 63 | community. 64 | 65 | ## Releases 66 | 67 | We use semantic versioning when releasing the bot. Once merged into the master 68 | branch, we will release a new maintenance or _patch_ version of the project, 69 | followed by a new maintentance release of its associated docker repository. 70 | 71 | As such: 72 | 73 | - _fixes_ will bump the patch version: e.g., 1.2.3 will bump to 1.2.4. 74 | - _features_ that contain no breaking changes will bump the minor version: e.g., 75 | 1.2.3 will bump to 1.3.0. 76 | - _breaking changes_ will bump the major version: e.g., 1.2.3 would bump to 2.0.0. 77 | 78 | ## Working with the bot 79 | 80 | Since this is a chatbot, you will need to do some functional testing! 81 | 82 | ### Dependencies 83 | 84 | Hubot is written in node.js, and, currently, utilizies 85 | [coffeescript](http://coffeescript.org), a superset of JS. 86 | 87 | Dependencies are managed typically using the Node Package Manager, or npm. For 88 | this project, however, we are using [yarn](https://yarnpkg.com/), as this 89 | creates a lockfile, ensuring that any given install will match any other. This 90 | is particularly important when considering creation of the Docker container for 91 | the bot, as we want to ensure that it matches what was last tested. 92 | 93 | To install dependencies: 94 | 95 | ```bash 96 | $ yarn install 97 | ``` 98 | 99 | To add new dependencies, use `yarn add`, which will update both the 100 | `packages.json` as well as the `yarn.lock` files. 101 | 102 | ### Tokens and integrations 103 | 104 | Testing the bot functionally requires some setup. First, you will need: 105 | 106 | - A sandbox Slack to play in. Create one of your own, or ask @weierophinney for 107 | access to his. 108 | - If you are using your own Slack, you will need to add the Hubot integration to 109 | your Slack. Make a note of the generated API token, the bot's name, and the 110 | slack name to which you will connect. 111 | - Twitter consumer key and secret, and access token key and secret. For this, 112 | you will need to create a Twitter app integration via https://apps.twitter.com. 113 | - A GitHub personal access token: https://github.com/settings/tokens 114 | 115 | With that information: 116 | 117 | - Copy `.env.dist` to `.env`. 118 | - Fill in the details of `.env` based on the information you've gathered and/or 119 | created. 120 | 121 | ### Docker 122 | 123 | You can use [docker-compose](https://docs.docker.com/compose/) to fire up your 124 | instance. By default: 125 | 126 | - The bot itself will be listening on port 9001 for any payloads that are 127 | expected to come from the web; these might be github webhooks, etc. 128 | - An nginx reverse proxy will listen on port 8080, and forward requests to the 129 | bot. At the time of writing, any paths not matching `/github/` will be served 130 | a static page, so you may need to alter the `etc/ngnix/nginx.dev.conf` and/or 131 | `etc/nginx/nginx.conf` files to allow other paths. 132 | - Redis will be listening on port 6379. 133 | 134 | The project contains two `Dockerfile`s: 135 | 136 | - `etc/docker/hubot.Dockerfile` details the container for the bot. 137 | - `etc/docker/nginx.Dockerfile` details the nginx reverse proxy configuration 138 | used in production. 139 | 140 | The `docker-compose.yml` file details how the various containers relate in 141 | development, and the `docker-stack.yml` file configures the containers for 142 | production. In most cases, you should not need to make changes to this latter 143 | file. 144 | 145 | You can _override_ or _add to_ settings in the `docker-compose.yml` by creating 146 | a `docker-compose.override.yml` file. With recent versions of `docker-compose`, 147 | these are now merged together automatically. We provide a `.gitignore` rule to 148 | ensure the override file is not contributed accidently. 149 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | https://{$CADDY_WEB_HOST} { 2 | proxy / nginx:8080 3 | header / Strict-Transport-Security "max-age=31536000;" 4 | tls {$CADDY_TLS_EMAIL} 5 | } 6 | 7 | http://{$CADDY_WEB_HOST} { 8 | redir https://{$CADDY_WEB_HOST}{uri} 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Matthew Weier O'Phinney 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # zfbot.mwop.net Makefile 2 | # 3 | # Create a docker-stack.yml based on latest tags of required containers, and 4 | # deploy to swarm. 5 | # 6 | # Allowed/expected variables: 7 | # 8 | # - CADDY_VERSION: specific caddy container version to use 9 | # - NGINX_VERSION: specific nginx container version to use 10 | # - ZFBOT_VERSION: specific zfbot container version to use 11 | # 12 | # If not specified, each defaults to "latest", which forces a lookup of the 13 | # latest tagged version. 14 | 15 | VERSION := $(shell date +%Y%m%d%H%M) 16 | 17 | CADDY_VERSION?=latest 18 | NGINX_VERSION?=latest 19 | ZFBOT_VERSION?=latest 20 | 21 | .PHONY : all deploy caddy nginx zfbot 22 | 23 | all: check-env deploy caddy nginx zfbot 24 | 25 | check-env: 26 | ifndef DOCKER_MACHINE_NAME 27 | $(error DOCKER_MACHINE_NAME is undefined; run "eval $$(docker-machine env zfbot)" first) 28 | endif 29 | ifneq ($(DOCKER_MACHINE_NAME),zfbot) 30 | $(error DOCKER_MACHINE_NAME is incorrect; run "eval $$(docker-machine env zfbot)" first) 31 | endif 32 | 33 | docker-stack.yml: 34 | @echo "Creating docker-stack.yml" 35 | @echo "- caddy container version: $(CADDY_VERSION)" 36 | @echo "- nginx container version: $(NGINX_VERSION)" 37 | @echo "- zfbot container version: $(ZFBOT_VERSION)" 38 | - $(CURDIR)/bin/create-docker-stack.php -n $(NGINX_VERSION) -b $(ZFBOT_VERSION) -c ${CADDY_VERSION} 39 | 40 | deploy: check-env docker-stack.yml 41 | @echo "Deploying to swarm" 42 | - docker stack deploy --with-registry-auth -c docker-stack.yml zfbot 43 | - rm docker-stack.yml 44 | 45 | nginx: 46 | @echo "Creating nginx container" 47 | @echo "- Building container" 48 | - docker build -t zfbot-nginx -f ./etc/docker/nginx.Dockerfile . 49 | @echo "- Tagging image" 50 | - docker tag zfbot-nginx:latest mwop/zfbot-nginx:$(VERSION) 51 | @echo "- Pushing image to hub" 52 | - docker push mwop/zfbot-nginx:$(VERSION) 53 | 54 | caddy: 55 | @echo "Creating caddy container" 56 | @echo "- Building container" 57 | - docker build -t zfbot-caddy -f ./etc/docker/caddy.Dockerfile . 58 | @echo "- Tagging image" 59 | - docker tag zfbot-caddy:latest mwop/zfbot-caddy:$(VERSION) 60 | @echo "- Pushing image to hub" 61 | - docker push mwop/zfbot-caddy:$(VERSION) 62 | 63 | zfbot: 64 | @echo "Creating zfbot container" 65 | @echo "- Building container" 66 | - docker build -t zfbot -f ./etc/docker/hubot.Dockerfile . 67 | @echo "- Tagging image" 68 | - docker tag zfbot:latest mwop/zfbot:$(VERSION) 69 | @echo "- Pushing image to hub" 70 | - docker push mwop/zfbot:$(VERSION) 71 | -------------------------------------------------------------------------------- /README-hubot.md: -------------------------------------------------------------------------------- 1 | # zf-hubot 2 | 3 | zf-hubot is a chat bot built on the [Hubot][hubot] framework. It was 4 | initially generated by [generator-hubot][generator-hubot], and configured to be 5 | deployed on [Heroku][heroku] to get you up and running as quick as possible. 6 | 7 | This README is intended to help get you started. Definitely update and improve 8 | to talk about your own instance, how to use and deploy, what functionality is 9 | available, etc! 10 | 11 | [heroku]: http://www.heroku.com 12 | [hubot]: http://hubot.github.com 13 | [generator-hubot]: https://github.com/github/generator-hubot 14 | 15 | ### Running zf-hubot Locally 16 | 17 | You can test your hubot by running the following, however some plugins will not 18 | behave as expected unless the [environment variables](#configuration) they rely 19 | upon have been set. 20 | 21 | You can start zf-hubot locally by running: 22 | 23 | % bin/hubot 24 | 25 | You'll see some start up output and a prompt: 26 | 27 | [Sat Feb 28 2015 12:38:27 GMT+0000 (GMT)] INFO Using default redis on localhost:6379 28 | zf-hubot> 29 | 30 | Then you can interact with zf-hubot by typing `zf-hubot help`. 31 | 32 | zf-hubot> zf-hubot help 33 | zf-hubot animate me - The same thing as `image me`, except adds [snip] 34 | zf-hubot help - Displays all of the help commands that zf-hubot knows about. 35 | ... 36 | 37 | ### Configuration 38 | 39 | A few scripts (including some installed by default) require environment 40 | variables to be set as a simple form of configuration. 41 | 42 | Each script should have a commented header which contains a "Configuration" 43 | section that explains which values it requires to be placed in which variable. 44 | When you have lots of scripts installed this process can be quite labour 45 | intensive. The following shell command can be used as a stop gap until an 46 | easier way to do this has been implemented. 47 | 48 | grep -o 'hubot-[a-z0-9_-]\+' external-scripts.json | \ 49 | xargs -n1 -I {} sh -c 'sed -n "/^# Configuration/,/^#$/ s/^/{} /p" \ 50 | $(find node_modules/{}/ -name "*.coffee")' | \ 51 | awk -F '#' '{ printf "%-25s %s\n", $1, $2 }' 52 | 53 | How to set environment variables will be specific to your operating system. 54 | Rather than recreate the various methods and best practices in achieving this, 55 | it's suggested that you search for a dedicated guide focused on your OS. 56 | 57 | ### Scripting 58 | 59 | An example script is included at `scripts/example.coffee`, so check it out to 60 | get started, along with the [Scripting Guide][scripting-docs]. 61 | 62 | For many common tasks, there's a good chance someone has already one to do just 63 | the thing. 64 | 65 | [scripting-docs]: https://github.com/github/hubot/blob/master/docs/scripting.md 66 | 67 | ### external-scripts 68 | 69 | There will inevitably be functionality that everyone will want. Instead of 70 | writing it yourself, you can use existing plugins. 71 | 72 | Hubot is able to load plugins from third-party `npm` packages. This is the 73 | recommended way to add functionality to your hubot. You can get a list of 74 | available hubot plugins on [npmjs.com][npmjs] or by using `npm search`: 75 | 76 | % npm search hubot-scripts panda 77 | NAME DESCRIPTION AUTHOR DATE VERSION KEYWORDS 78 | hubot-pandapanda a hubot script for panda responses =missu 2014-11-30 0.9.2 hubot hubot-scripts panda 79 | ... 80 | 81 | 82 | To use a package, check the package's documentation, but in general it is: 83 | 84 | 1. Use `npm install --save` to add the package to `package.json` and install it 85 | 2. Add the package name to `external-scripts.json` as a double quoted string 86 | 87 | You can review `external-scripts.json` to see what is included by default. 88 | 89 | ##### Advanced Usage 90 | 91 | It is also possible to define `external-scripts.json` as an object to 92 | explicitly specify which scripts from a package should be included. The example 93 | below, for example, will only activate two of the six available scripts inside 94 | the `hubot-fun` plugin, but all four of those in `hubot-auto-deploy`. 95 | 96 | ```json 97 | { 98 | "hubot-fun": [ 99 | "crazy", 100 | "thanks" 101 | ], 102 | "hubot-auto-deploy": "*" 103 | } 104 | ``` 105 | 106 | **Be aware that not all plugins support this usage and will typically fallback 107 | to including all scripts.** 108 | 109 | [npmjs]: https://www.npmjs.com 110 | 111 | ### hubot-scripts 112 | 113 | Before hubot plugin packages were adopted, most plugins were held in the 114 | [hubot-scripts][hubot-scripts] package. Some of these plugins have yet to be 115 | migrated to their own packages. They can still be used but the setup is a bit 116 | different. 117 | 118 | To enable scripts from the hubot-scripts package, add the script name with 119 | extension as a double quoted string to the `hubot-scripts.json` file in this 120 | repo. 121 | 122 | [hubot-scripts]: https://github.com/github/hubot-scripts 123 | 124 | ## Persistence 125 | 126 | If you are going to use the `hubot-redis-brain` package (strongly suggested), 127 | you will need to add the Redis to Go addon on Heroku which requires a verified 128 | account or you can create an account at [Redis to Go][redistogo] and manually 129 | set the `REDISTOGO_URL` variable. 130 | 131 | % heroku config:add REDISTOGO_URL="..." 132 | 133 | If you don't need any persistence feel free to remove the `hubot-redis-brain` 134 | from `external-scripts.json` and you don't need to worry about redis at all. 135 | 136 | [redistogo]: https://redistogo.com/ 137 | 138 | ## Adapters 139 | 140 | Adapters are the interface to the service you want your hubot to run on, such 141 | as Campfire or IRC. There are a number of third party adapters that the 142 | community have contributed. Check [Hubot Adapters][hubot-adapters] for the 143 | available ones. 144 | 145 | If you would like to run a non-Campfire or shell adapter you will need to add 146 | the adapter package as a dependency to the `package.json` file in the 147 | `dependencies` section. 148 | 149 | Once you've added the dependency with `npm install --save` to install it you 150 | can then run hubot with the adapter. 151 | 152 | % bin/hubot -a 153 | 154 | Where `` is the name of your adapter without the `hubot-` prefix. 155 | 156 | [hubot-adapters]: https://github.com/github/hubot/blob/master/docs/adapters.md 157 | 158 | ## Deployment 159 | 160 | % heroku create --stack cedar 161 | % git push heroku master 162 | 163 | If your Heroku account has been verified you can run the following to enable 164 | and add the Redis to Go addon to your app. 165 | 166 | % heroku addons:add redistogo:nano 167 | 168 | If you run into any problems, checkout Heroku's [docs][heroku-node-docs]. 169 | 170 | You'll need to edit the `Procfile` to set the name of your hubot. 171 | 172 | More detailed documentation can be found on the [deploying hubot onto 173 | Heroku][deploy-heroku] wiki page. 174 | 175 | ### Deploying to UNIX or Windows 176 | 177 | If you would like to deploy to either a UNIX operating system or Windows. 178 | Please check out the [deploying hubot onto UNIX][deploy-unix] and [deploying 179 | hubot onto Windows][deploy-windows] wiki pages. 180 | 181 | [heroku-node-docs]: http://devcenter.heroku.com/articles/node-js 182 | [deploy-heroku]: https://github.com/github/hubot/blob/master/docs/deploying/heroku.md 183 | [deploy-unix]: https://github.com/github/hubot/blob/master/docs/deploying/unix.md 184 | [deploy-windows]: https://github.com/github/hubot/blob/master/docs/deploying/windows.md 185 | 186 | ## Campfire Variables 187 | 188 | If you are using the Campfire adapter you will need to set some environment 189 | variables. If not, refer to your adapter documentation for how to configure it, 190 | links to the adapters can be found on [Hubot Adapters][hubot-adapters]. 191 | 192 | Create a separate Campfire user for your bot and get their token from the web 193 | UI. 194 | 195 | % heroku config:add HUBOT_CAMPFIRE_TOKEN="..." 196 | 197 | Get the numeric IDs of the rooms you want the bot to join, comma delimited. If 198 | you want the bot to connect to `https://mysubdomain.campfirenow.com/room/42` 199 | and `https://mysubdomain.campfirenow.com/room/1024` then you'd add it like 200 | this: 201 | 202 | % heroku config:add HUBOT_CAMPFIRE_ROOMS="42,1024" 203 | 204 | Add the subdomain hubot should connect to. If you web URL looks like 205 | `http://mysubdomain.campfirenow.com` then you'd add it like this: 206 | 207 | % heroku config:add HUBOT_CAMPFIRE_ACCOUNT="mysubdomain" 208 | 209 | [hubot-adapters]: https://github.com/github/hubot/blob/master/docs/adapters.md 210 | 211 | ## Restart the bot 212 | 213 | You may want to get comfortable with `heroku logs` and `heroku restart` if 214 | you're having issues. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zfbot 2 | 3 | This repository contains the scripts and library code for the 4 | [Zend Framework Slack](https://zendframework-slack.herokuapp.com)'s hubot 5 | instance, which provides integrations for the slack channel specifically, 6 | including: 7 | 8 | - Reacting to GitHub webhooks and posting relevant information to the Slack. 9 | - Reacting to Discourse webhooks and posting relevant information to the Slack. 10 | - Following twitter users and/or searches, per channel in the Slack. 11 | - Posting tweets via a configured user. 12 | - Posting release announcements to Slack. 13 | - Broadcasting release announcements via a webhook. 14 | - Building documentation on successful builds of watched GitHub repositories. 15 | 16 | Interested? 17 | 18 | - [Contribute](CONTRIBUTING.md) 19 | - [Play nice!](CODE_OF_CONDUCT.md) 20 | - [Understand where/how you can use this code](LICENSE.md) 21 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | This document is only for those who have access to the server, and the 4 | production tokens. 5 | 6 | ## Creating images 7 | 8 | - zfbot: 9 | ```bash 10 | $ make zfbot 11 | ``` 12 | Note: requires a `.env` file with all appropriate tokens; see `.env.dist` for 13 | a template. 14 | 15 | - zfbot-nginx: 16 | ```bash 17 | $ make nginx 18 | ``` 19 | 20 | - zfbot-caddy: 21 | ```bash 22 | $ make caddy 23 | ``` 24 | Note: requires a `.caddy.env` file with definitions for the env vars 25 | `CADDY_WEB_HOST` and `CADDY_TLS_EMAIL`. 26 | 27 | ## Deployment 28 | 29 | Several things to remember: 30 | 31 | - `eval $(docker-machine env zfbot)` 32 | - If never before deployed, run `docker swarm init --advertise-addr $(docker-machine url zfbot | sed 's#tcp://##' | sed -r 's#:[0-9]+$##')` 33 | 34 | I had to create networks for each of `public` and `server` (a) to allow the 35 | containers to talk to each other, and (b) to expose a network publicly. I used 36 | `docker network create --driver=overlay --attachable {network-name}` to do this 37 | in each case. This must be done in swarm mode! 38 | 39 | The above are all necessary to ensure that the environment is correctly 40 | initialized before deployment. If you've done multiple releases between logins 41 | and within the same shell, you may get messages saying these steps have already 42 | been done; don't take that for granted, though! 43 | 44 | Once ready: 45 | 46 | ```bash 47 | $ make deploy 48 | ``` 49 | 50 | Typically, this will only update containers with updated images, or where 51 | configuration in `docker-stack.yml.dist` has occurred. 52 | -------------------------------------------------------------------------------- /bin/create-docker-stack.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $argv[2], 37 | 'zfbot' => $argv[4], 38 | 'zfbot-caddy' => $argv[6], 39 | ]; 40 | 41 | $substitutions = []; 42 | foreach (REPOS as $repo) { 43 | // Was a version provided for this repo? 44 | if ($versions[$repo] !== 'latest') { 45 | $substitutions[sprintf('{%s}', $repo)] = $versions[$repo]; 46 | continue; 47 | } 48 | 49 | // Look up the latest tagged version for this repo 50 | $command = sprintf('%s %s %s', TAGSCRIPT, USER, $repo); 51 | exec($command, $output, $return); 52 | if ($return !== 0) { 53 | fwrite(STDERR, implode($output, PHP_EOL)); 54 | exit($return); 55 | } 56 | 57 | $substitutions[sprintf('{%s}', $repo)] = array_shift($output); 58 | } 59 | 60 | $stackFile = file_get_contents(TEMPLATE); 61 | $stackFile = str_replace(array_keys($substitutions), array_values($substitutions), $stackFile); 62 | 63 | file_put_contents(STACKFILE, $stackFile); 64 | 65 | function usage($stream, string $scriptName) 66 | { 67 | $message = <<<'EOM' 68 | Usage: 69 | 70 | %s -n -b -c 71 | 72 | where: 73 | 74 | Version tag of nginx container to use 75 | Version tag of zfbot container to use 76 | Version tag of caddy container to use 77 | 78 | Generates the docker-stack.yml file to use during deployment, using the 79 | specified tags for the nginx and php-fpm containers. 80 | 81 | In either case, if the string "latest" is used, this script will look up 82 | the latest tagged version of that container and use that to generate the 83 | docker-stack.yml file. 84 | 85 | EOM; 86 | 87 | $message = sprintf($message, $scriptName); 88 | $message = sprintf("\n", PHP_EOL, $message); 89 | fwrite($stream, $message); 90 | } 91 | -------------------------------------------------------------------------------- /bin/get-latest-tag.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | results) || 0 === count($data->results)) { 14 | fwrite(STDERR, sprintf("No tags found for %s/%s%s", $user, $repo, PHP_EOL)); 15 | exit(1); 16 | } 17 | 18 | $mostRecent = null; 19 | $lastUpdated = null; 20 | foreach ($data->results as $result) { 21 | if (null === $mostRecent) { 22 | $mostRecent = $result->name; 23 | $lastUpdated = new DateTime($result->last_updated); 24 | continue; 25 | } 26 | 27 | $test = new DateTime($result->last_updated); 28 | if ($test > $lastUpdated) { 29 | $mostRecent = $result->name; 30 | $lastUpdated = $test; 31 | continue; 32 | } 33 | } 34 | 35 | fwrite(STDOUT, $mostRecent); 36 | exit(0); 37 | -------------------------------------------------------------------------------- /bin/hubot: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | npm install 6 | export PATH="node_modules/.bin:node_modules/hubot/node_modules/.bin:$PATH" 7 | 8 | exec node_modules/.bin/hubot --name "zf-hubot" "$@" 9 | -------------------------------------------------------------------------------- /bin/hubot.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | call npm install 4 | SETLOCAL 5 | SET PATH=node_modules\.bin;node_modules\hubot\node_modules\.bin;%PATH% 6 | 7 | node_modules\.bin\hubot.cmd --name "zf-hubot" %* 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | redis: 5 | image: "redis:alpine" 6 | environment: 7 | - REDIS_APPENDONLY=yes 8 | - REDIS_APPENDFSYNC=always 9 | ports: 10 | - "6379:6379" 11 | volumes: 12 | - ./data:/data 13 | 14 | hubot: 15 | build: 16 | context: . 17 | dockerfile: ./etc/docker/hubot.Dockerfile 18 | depends_on: 19 | - redis 20 | env_file: 21 | - .env 22 | ports: 23 | - "9001:9001" 24 | volumes: 25 | - type: volume 26 | source: . 27 | target: /hubot 28 | 29 | nginx: 30 | image: nginx 31 | depends_on: 32 | - hubot 33 | ports: 34 | - "8080:80" 35 | - "9443:443" 36 | volumes: 37 | - ./etc/nginx/nginx.conf:/etc/nginx/nginx.conf 38 | - ./var/www/index.html:/var/www/index.html 39 | -------------------------------------------------------------------------------- /docker-stack.yml.dist: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | services: 3 | redis: 4 | image: "redis:alpine" 5 | environment: 6 | - REDIS_APPENDONLY=yes 7 | - REDIS_APPENDFSYNC=always 8 | networks: 9 | - server 10 | volumes: 11 | - /data:/data 12 | 13 | hubot: 14 | image: "mwop/zfbot:{zfbot}" 15 | depends_on: 16 | - redis 17 | env_file: 18 | - .env 19 | networks: 20 | - server 21 | 22 | nginx: 23 | image: "mwop/zfbot-nginx:{zfbot-nginx}" 24 | depends_on: 25 | - hubot 26 | networks: 27 | - server 28 | 29 | caddy: 30 | image: "mwop/zfbot-caddy:{zfbot-caddy}" 31 | env_file: 32 | - .caddy.env 33 | depends_on: 34 | - nginx 35 | restart: on-failure 36 | volumes: 37 | - /data/caddy:/root/.caddy 38 | ports: 39 | - "80:80" 40 | - "443:443" 41 | networks: 42 | - server 43 | - public 44 | 45 | networks: 46 | public: 47 | external: true 48 | server: 49 | -------------------------------------------------------------------------------- /etc/docker/caddy.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM abiosoft/caddy:0.10.11 2 | 3 | ADD Caddyfile /etc/Caddyfile 4 | -------------------------------------------------------------------------------- /etc/docker/hubot.Dockerfile: -------------------------------------------------------------------------------- 1 | # DOCKER-VERSION 1.3.2 2 | 3 | FROM ubuntu:artful 4 | 5 | ENV LC_ALL=C.UTF-8 6 | ENV LANG=C.UTF-8 7 | 8 | RUN apt-get update 9 | RUN apt-get -y install apt-utils apt-transport-https build-essential curl 10 | RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - 11 | RUN apt-get -y install nodejs git python3-pip php7.1-cli gcc g++ make 12 | RUN pip3 install mkdocs pymdown-extensions markdown-fenced-code-tabs pyaml 13 | 14 | RUN mkdir /hubot 15 | ADD bin /hubot/bin 16 | ADD img /hubot/img 17 | ADD lib /hubot/lib 18 | ADD scripts /hubot/scripts 19 | COPY external-scripts.json /hubot/ 20 | COPY package.json /hubot/ 21 | COPY package-lock.json /hubot/ 22 | 23 | RUN cd /hubot && npm install --no-save 24 | 25 | EXPOSE 9001 26 | 27 | WORKDIR /hubot 28 | 29 | CMD ["bin/hubot"] 30 | 31 | ENTRYPOINT ["/bin/bash"] 32 | -------------------------------------------------------------------------------- /etc/docker/nginx.Dockerfile: -------------------------------------------------------------------------------- 1 | # DOCKER-VERSION 1.3.2 2 | 3 | FROM nginx:1.13 4 | 5 | COPY ./etc/nginx/nginx.conf /etc/nginx/ 6 | COPY ./var/www/index.html /var/www/ 7 | 8 | EXPOSE 8080 9 | CMD ["nginx", "-g", "daemon off;"] 10 | -------------------------------------------------------------------------------- /etc/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | events { worker_connections 1024; } 3 | http { 4 | upstream hubot-app { 5 | least_conn; 6 | server hubot:9001 weight=10 max_fails=3 fail_timeout=30s; 7 | } 8 | 9 | server { 10 | listen 8080; 11 | server_name zfbot.mwop.net; 12 | 13 | location ~ ^/(github|discourse|docs) { 14 | proxy_pass http://hubot-app; 15 | proxy_http_version 1.1; 16 | proxy_set_header Upgrade $http_upgrade; 17 | proxy_set_header Connection 'upgrade'; 18 | proxy_set_header Host $host; 19 | proxy_cache_bypass $http_upgrade; 20 | } 21 | 22 | location / { 23 | root /var/www; 24 | try_files $uri /index.html; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /etc/nginx/nginx.dev.conf: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | events { worker_connections 1024; } 3 | http { 4 | upstream hubot-app { 5 | least_conn; 6 | server hubot:9001 weight=10 max_fails=3 fail_timeout=30s; 7 | } 8 | 9 | server { 10 | listen 80; 11 | 12 | location ~ ^/(github|discourse|docs) { 13 | proxy_pass http://hubot-app; 14 | proxy_http_version 1.1; 15 | proxy_set_header Upgrade $http_upgrade; 16 | proxy_set_header Connection 'upgrade'; 17 | proxy_set_header Host $host; 18 | proxy_cache_bypass $http_upgrade; 19 | } 20 | 21 | location / { 22 | root /var/www; 23 | try_files $uri /index.html; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /external-scripts.json: -------------------------------------------------------------------------------- 1 | [ 2 | "hubot-diagnostics", 3 | "hubot-help", 4 | "hubot-redis-brain" 5 | ] 6 | -------------------------------------------------------------------------------- /img/zf-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendframework/zfbot/6a4f4c41104fb298e697d8ce93c8046c639f119a/img/zf-logo.png -------------------------------------------------------------------------------- /lib/discourse-categories.coffee: -------------------------------------------------------------------------------- 1 | # Information about Discourse categories 2 | # 3 | # This is necessary as the Discourse API only reveals top-level categories; we 4 | # have sub-categories. 5 | 6 | module.exports = [ 7 | { 8 | id: 1 9 | name: "Uncategorized" 10 | slug: "uncategorized" 11 | } 12 | { 13 | id: 10 14 | name: "Contributors" 15 | slug: "contributors" 16 | } 17 | { 18 | id: 9 19 | name: "Show and Tell" 20 | slug: "show-and-tell" 21 | } 22 | { 23 | id: 6 24 | name: "Components" 25 | slug: "questions/components" 26 | } 27 | { 28 | id: 7 29 | name: "Expressive" 30 | slug: "questions/expressive" 31 | } 32 | { 33 | id: 8 34 | name: "Apigility" 35 | slug: "questions/apigility" 36 | } 37 | ] 38 | -------------------------------------------------------------------------------- /lib/discourse-category.coffee: -------------------------------------------------------------------------------- 1 | # Retrieve information on a single Discourse category 2 | # 3 | # Checks the robot.brain for categories, and then attempts to match the 4 | # requested category identifier to one that's known. 5 | 6 | _ = require "lodash" 7 | categories = require "./discourse-categories" 8 | 9 | default_category = { 10 | name: "Uncategorized" 11 | slug: "uncategorized" 12 | id: 1 13 | } 14 | 15 | module.exports = (robot, category_id) -> 16 | robot.logger.info "Looking for #{category_id} in categories", categories 17 | if not categories?.length 18 | robot.logger.error "Discourse categories have not been loaded?" 19 | return default_category 20 | 21 | category = _.find categories, (data) -> 22 | return data.id == category_id 23 | 24 | return default_category if not category 25 | return category 26 | -------------------------------------------------------------------------------- /lib/discourse-post.coffee: -------------------------------------------------------------------------------- 1 | # Handle an incoming Discourse post (comment) 2 | 3 | formatter = require "slackify-html" 4 | 5 | module.exports = (robot, room, discourse_url, payload) -> 6 | return if not payload?.post? 7 | post = payload.post 8 | 9 | return if post.hidden 10 | return if post.deleted_at 11 | 12 | # Uncomment to allow broadcast of edit events 13 | return if post.created_at != post.updated_at 14 | 15 | action = if post.created_at == post.updated_at then "created" else "edited" 16 | 17 | url = "#{discourse_url}/t/#{post.topic_slug}/#{post.topic_id}/#{post.id}" 18 | 19 | ts = if action == "created" then post.created_at else post.updated_at 20 | ts = new Date ts 21 | ts = Math.floor(ts.getTime() / 1000) 22 | 23 | user_link = "<#{discourse_url}/u/#{post.username}|#{post.name}>" 24 | topic_link = "<#{discourse_url}/t/#{post.topic_slug}/#{post.topic_id}|#{post.topic_title}>" 25 | 26 | fields = [ 27 | { 28 | title: "In reply to" 29 | value: topic_link 30 | short: true 31 | } 32 | { 33 | title: "Posted by" 34 | value: user_link 35 | short: true 36 | } 37 | ] 38 | 39 | attachment = 40 | attachments: [ 41 | color: "#295473" 42 | fallback: "Discourse: Comment #{action} for #{post.topic_title}: #{url}" 43 | author_name: "Discourse" 44 | author_link: discourse_url 45 | author_icon: "https://slack-imgs.com/?c=1&o1=wi16.he16&url=https%3A%2F%2Fdiscourse-meta.s3-us-west-1.amazonaws.com%2Foriginal%2F3X%2Fc%2Fb%2Fcb4bec8901221d4a646e45e1fa03db3a65e17f59.png" 46 | title: "Comment #{action} for #{post.topic_title} by #{post.name}" 47 | title_link: url 48 | text: formatter post.cooked 49 | fields: fields 50 | footer: "Discourse" 51 | footer_icon: "https://slack-imgs.com/?c=1&o1=wi16.he16&url=https%3A%2F%2Fdiscourse-meta.s3-us-west-1.amazonaws.com%2Foriginal%2F3X%2Fc%2Fb%2Fcb4bec8901221d4a646e45e1fa03db3a65e17f59.png" 52 | ts: ts 53 | ] 54 | 55 | robot.send room: room, attachment 56 | -------------------------------------------------------------------------------- /lib/discourse-topic.coffee: -------------------------------------------------------------------------------- 1 | # Handle an incoming Discourse topic 2 | 3 | discourse_category = require "./discourse-category" 4 | 5 | module.exports = (robot, room, discourse_url, payload) -> 6 | return if not payload?.topic? 7 | topic = payload.topic 8 | 9 | return if not topic.visible 10 | return if topic.draft? 11 | 12 | action = if topic.posts_count == 1 then "created" else "edited" 13 | 14 | url = "#{discourse_url}/t/#{topic.slug}/#{topic.id}" 15 | 16 | ts = if action == "created" then topic.created_at else topic.last_posted_at 17 | ts = new Date ts 18 | ts = Math.floor(ts.getTime() / 1000) 19 | 20 | user_name = topic.details.created_by.username 21 | user_link = "<#{discourse_url}/u/#{user_name}|#{user_name}>" 22 | 23 | category = discourse_category(robot, topic.category_id) 24 | 25 | fields = [ 26 | { 27 | title: "Category" 28 | value: "<#{discourse_url}/c/#{category.slug}|#{category.name}>" 29 | short: true 30 | } 31 | { 32 | title: "Posted by" 33 | value: user_link 34 | short: true 35 | } 36 | ] 37 | 38 | if topic.tags.length > 0 39 | tags = [] 40 | topic.tags.forEach (tag) => tags.push "- <#{discourse_url}/tags/#{tag}|#{tag}>" 41 | fields.push { 42 | title: "Tags" 43 | value: tags.join "\n" 44 | } 45 | 46 | attachment = 47 | attachments: [ 48 | color: "#295473" 49 | fallback: "Discourse: Topic #{action} in #{category.name}: #{url}" 50 | author_name: "Discourse" 51 | author_link: discourse_url 52 | author_icon: "https://slack-imgs.com/?c=1&o1=wi16.he16&url=https%3A%2F%2Fdiscourse-meta.s3-us-west-1.amazonaws.com%2Foriginal%2F3X%2Fc%2Fb%2Fcb4bec8901221d4a646e45e1fa03db3a65e17f59.png" 53 | title: "[#{category.name}] Topic #{action}: #{topic.fancy_title}" 54 | title_link: url 55 | text: "Topic #{action} in category <#{discourse_url}/c/#{category.slug}|#{category.name}>: <#{url}|#{topic.fancy_title}>" 56 | fields: fields 57 | footer: "Discourse" 58 | footer_icon: "https://slack-imgs.com/?c=1&o1=wi16.he16&url=https%3A%2F%2Fdiscourse-meta.s3-us-west-1.amazonaws.com%2Foriginal%2F3X%2Fc%2Fb%2Fcb4bec8901221d4a646e45e1fa03db3a65e17f59.png" 59 | ts: ts 60 | ] 61 | 62 | robot.send room: room, attachment 63 | -------------------------------------------------------------------------------- /lib/discourse-verify-signature.coffee: -------------------------------------------------------------------------------- 1 | # Verify a Discourse webhook payload signature 2 | 3 | crypto = require "crypto" 4 | 5 | module.exports = (req, secret) -> 6 | return false if not req.headers? 7 | return false if not req.headers.hasOwnProperty "x-discourse-event-signature" 8 | 9 | header = req.headers["x-discourse-event-signature"] 10 | signature = if header.match /^sha256\=/ then header.substring 7 else header 11 | 12 | compare = crypto.createHmac('sha256', secret).update(req.rawBody, 'utf-8').digest('hex') 13 | return signature == compare.toString() 14 | -------------------------------------------------------------------------------- /lib/docs-build.coffee: -------------------------------------------------------------------------------- 1 | # Build and push documentation on release. 2 | 3 | exec = require("child_process").exec 4 | fs = require "fs" 5 | rimraf = require "rimraf" 6 | 7 | class DocsBuild 8 | constructor: (@robot, @gh_user, @gh_email, @gh_token) -> 9 | 10 | canBuild: null 11 | 12 | build: (repo_name, msg) -> 13 | if false == @canBuild 14 | msg.send "Build requirements are missing; aborting" if msg 15 | return 16 | if null == @canBuild and not @verifyDeps 17 | msg.send "Build requirements are missing; aborting" if msg 18 | return 19 | 20 | # Only interested in zendframework repos currently 21 | [org, repo] = repo_name.split "/" 22 | if org != "zendframework" 23 | msg.send "I can only build documentation for zendframework repositories" if msg 24 | return 25 | 26 | # If a build path already exists, there was an error previously 27 | if fs.existsSync "/tmp/#{repo}" 28 | @robot.logger.error "[docs-build] Build path for #{repo} already exists" 29 | msg.send "It looks like a previous build failed to clean up; aborting." if msg 30 | return 31 | 32 | msg.send "Preparing to build documentation for #{repo}... I will let you know when I'm done." if msg 33 | 34 | # Clone the repository to a build path 35 | exec "git clone -b master git://github.com/#{org}/#{repo}.git", {cwd: "/tmp"}, (error, stdout, stderr) => 36 | if error 37 | @robot.logger.error "[docs-build] Error cloning repo #{repo}: #{error}" 38 | @robot.logger.error "[docs-build] #{stderr}" 39 | msg.send "It looks like an error occurred cloning the repository #{repo}; aborting." if msg 40 | return 41 | 42 | # If the repo does not have mkdocs.yml, nothing to do 43 | if not fs.existsSync "/tmp/#{repo}/mkdocs.yml" 44 | msg.send "The repository #repo does not have documentation to build." if msg 45 | @cleanUp repo 46 | return 47 | 48 | # Clone the mkdocs theme 49 | exec "git clone git://github.com/zendframework/zf-mkdoc-theme.git", {cwd: "/tmp/#{repo}"}, (error, stdout, stderr) => 50 | if error 51 | @robot.logger.error "[docs-build] Error cloning mkdoc theme: #{error}" 52 | @robot.logger.error "[docs-build] #{stderr}" 53 | msg.send "It looks like an error occurred cloning the zf-mkdoc-theme repository; aborting." if msg 54 | @cleanUp repo 55 | return 56 | 57 | # Run the build script 58 | command = "./zf-mkdoc-theme/deploy.sh " 59 | command += "-n \"#{@gh_user}\" " 60 | command += "-e \"#{@gh_email}\" " 61 | command += "-t \"#{@gh_token}\" " 62 | command += "-r \"github.com/zendframework/#{repo}.git\" " 63 | command += "-u \"https://docs.zendframework.com/#{repo}\"" 64 | exec command, {cwd: "/tmp/#{repo}"}, (error, stdout, stderr) => 65 | if error 66 | @robot.logger.error "[docs-build] Error running zf-mkdoc-theme deploy.sh: #{error}" 67 | @robot.logger.error "[docs-build] #{stdout}" 68 | @robot.logger.error "[docs-build] #{stderr}" 69 | msg.send "An error occurred while building docs for #{repo}; try again later." if msg 70 | @cleanUp repo 71 | return 72 | # Cleanup when done 73 | @cleanUp repo 74 | @robot.logger.info "[docs-build] Built and deployed documentation for #{repo}" 75 | msg.send "Finished building documentation for #{repo}" if msg 76 | 77 | cleanUp: (repo) -> 78 | rimraf "/tmp/#{repo}", (error) => 79 | return if not error 80 | @robot.logger.error "[docs-build] Failed to remove directory /tmp/#{repo}: #{error}" 81 | 82 | verifyDeps: () -> 83 | if not fs.existsSync "/usr/bin/git" 84 | @canBuild = false 85 | @robot.logger.error "[docs-build] /usr/bin/git binary not found" 86 | return false 87 | if not fs.existsSync "/usr/local/bin/mkdocs" 88 | @canBuild = false 89 | @robot.logger.error "[docs-build] /usr/local/bin/mkdocs binary not found" 90 | return false 91 | if not fs.existsSync "/usr/bin/php" 92 | @canBuild = false 93 | @robot.logger.error "[docs-build] /usr/bin/php binary not found" 94 | return false 95 | @canBuild = true 96 | return true 97 | 98 | module.exports = DocsBuild 99 | -------------------------------------------------------------------------------- /lib/error-handler.coffee: -------------------------------------------------------------------------------- 1 | # Default error handler for bot 2 | # 3 | # Replies with the error if a response is provided. 4 | # 5 | # In all cases, it logs the error. If a room is provided, it will message 6 | # that room with the error, so that the admin can see the issues. 7 | 8 | class ErrorHandler 9 | constructor: (@robot, @room) -> 10 | 11 | listen: (err, res) -> 12 | if res 13 | # If we have a response, reply 14 | res.reply "Oops! That's an error: #{err.message}" 15 | 16 | errorMessage = "ERROR:\n" 17 | errorMessage += if err.stack? then err.stack else err.toString() 18 | 19 | # Log the error 20 | @robot.logger.error errorMessage 21 | 22 | return if not @room 23 | 24 | # Message the configured room 25 | format = (line) -> return " " + line 26 | errorMessage = errorMessage.split("\n").map(format).join("\n") 27 | 28 | @robot.messageRoom @room, errorMessage 29 | 30 | module.exports = ErrorHandler 31 | -------------------------------------------------------------------------------- /lib/github-issue-comment.coffee: -------------------------------------------------------------------------------- 1 | # Handle the GitHub "issue_comment" event 2 | # 3 | # Usage: 4 | # 5 | # require('../lib/github-issue-comment')(robot, room, data) 6 | # 7 | # OR 8 | # 9 | # github_issue_comment = require '../lib/github-issue-comment' 10 | # github_issue_comment robot, room, data 11 | 12 | module.exports = (robot, room, payload) -> 13 | return if not payload.issue? 14 | return if not payload.action? 15 | return if not payload.action == "created" 16 | 17 | repo = payload.repository.full_name 18 | comment = payload.comment.body 19 | comment_url = payload.comment.html_url 20 | user_name = payload.comment.user.login 21 | user_url = payload.comment.user.html_url 22 | 23 | issue_id = payload.issue.number 24 | issue_url = payload.issue.html_url 25 | issue_title = payload.issue.title 26 | issue_type = if payload.issue.pull_request? then "pull request" else "issue" 27 | 28 | ts = new Date payload.comment.created_at 29 | ts = new Date ts.getTime() 30 | ts = Math.floor(ts.getTime() / 1000) 31 | 32 | attachment = 33 | attachments: [ 34 | fallback: "[#{repo}] New comment by #{user_name} on #{issue_type} ##{issue_id} #{issue_title}: #{comment_url}" 35 | color: "#FAD5A1" 36 | pretext: "[] New comment by <#{user_url}|#{user_name}> on #{issue_type} <#{comment_url}|##{issue_id} #{issue_title}>" 37 | author_name: "#{repo} (GitHub)" 38 | author_link: "https://github.com/#{repo}" 39 | author_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 40 | title: "Comment on #{issue_type} #{repo}##{issue_id}" 41 | title_link: comment_url 42 | text: comment 43 | fields: [ 44 | { 45 | title: "Repository" 46 | value: "" 47 | short: true 48 | } 49 | { 50 | title: "Commenter" 51 | value: "<#{user_url}|#{user_name}>" 52 | short: true 53 | } 54 | { 55 | title: "Issue" 56 | value: "<#{issue_url}|##{issue_id} #{issue_title}>" 57 | } 58 | ] 59 | footer: "GitHub" 60 | footer_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 61 | ts: ts 62 | ] 63 | 64 | robot.send room: room, attachment 65 | -------------------------------------------------------------------------------- /lib/github-issues.coffee: -------------------------------------------------------------------------------- 1 | # Handle the GitHub "issues" event 2 | # 3 | # Usage: 4 | # 5 | # require('../lib/github-issues')(robot, room, data) 6 | # 7 | # OR 8 | # 9 | # github_issues = require '../lib/github-issues' 10 | # github_issues robot, room, data 11 | 12 | module.exports = (robot, room, payload) -> 13 | return if not payload.issue? 14 | return if not payload.action? 15 | return if not payload.action in ["opened", "closed", "reopened"] 16 | 17 | action = payload.action 18 | repo = payload.repository.full_name 19 | user_name = payload.sender.login 20 | user_url = payload.sender.html_url 21 | issue_id = payload.issue.number 22 | issue_title = payload.issue.title 23 | issue_url = payload.issue.html_url 24 | issue_content = payload.issue.body 25 | 26 | switch action 27 | when "opened" then ts = new Date payload.issue.created_at 28 | when "closed" then ts = new Date payload.issue.closed_at 29 | when "reopened" then ts = new Date payload.issue.updated_at 30 | else return 31 | 32 | ts = new Date ts.getTime() 33 | ts = Math.floor(ts.getTime() / 1000) 34 | 35 | attachment = 36 | attachments: [ 37 | fallback: "[#{repo}] Issue ##{issue_id} #{action} by #{user_name}: #{issue_url}" 38 | color: "warning" 39 | pretext: "[] Issue #{action} by <#{user_url}|#{user_name}>" 40 | author_name: "#{repo} (GitHub)" 41 | author_link: "https://github.com/#{repo}" 42 | author_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 43 | title: "##{issue_id} #{issue_title}" 44 | title_link: issue_url 45 | text: issue_content 46 | fields: [ 47 | { 48 | title: "Repository" 49 | value: "" 50 | short: true 51 | } 52 | { 53 | title: "Reporter" 54 | value: "<#{user_url}|#{user_name}>" 55 | short: true 56 | } 57 | { 58 | title: "Status" 59 | value: action 60 | short: true 61 | } 62 | ] 63 | footer: "GitHub" 64 | footer_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 65 | ts: ts 66 | ] 67 | 68 | robot.send room: room, attachment 69 | -------------------------------------------------------------------------------- /lib/github-pull-request-review-comment.coffee: -------------------------------------------------------------------------------- 1 | # Handle the GitHub "pull_request_review_comment" event 2 | # 3 | # Usage: 4 | # 5 | # require('../lib/github-pull-request-review-comment')(robot, room, data) 6 | # 7 | # OR 8 | # 9 | # github_pull_request_review_comment = require # '../lib/github-pull-request-review-comment' 10 | # github_pull_request_review_comment robot, room, data 11 | 12 | module.exports = (robot, room, payload) -> 13 | return if not payload.comment? 14 | return if not payload.action? 15 | return if not payload.action == "created" 16 | 17 | content = payload.comment.body 18 | url = payload.comment.html_url 19 | 20 | repo = payload.repository.full_name 21 | user_name = payload.comment.user.login 22 | user_url = payload.comment.user.html_url 23 | pr_id = payload.pull_request.number 24 | pr_title = payload.pull_request.title 25 | pr_url = payload.pull_request.html_url 26 | 27 | ts = new Date payload.comment.created_at 28 | ts = new Date ts.getTime() 29 | ts = Math.floor(ts.getTime() / 1000) 30 | 31 | attachment = 32 | attachments: [ 33 | fallback: "[#{repo}] #{user_name} commented on pull request ##{pr_id}: #{url}" 34 | color: "#FAD5A1" 35 | pretext: "[] <#{user_url}|#{user_name}> commented on <#{pr_url}|##{pr_id} #{pr_title}>" 36 | author_name: "#{repo} (GitHub)" 37 | author_link: "https://github.com/#{repo}" 38 | author_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 39 | title: "Pull request review comment created for #{repo}##{pr_id} #{pr_title}" 40 | title_link: pr_url 41 | text: content 42 | fields: [ 43 | { 44 | title: "Repository" 45 | value: "" 46 | short: true 47 | } 48 | { 49 | title: "Commenter" 50 | value: "<#{user_url}|#{user_name}>" 51 | short: true 52 | } 53 | ] 54 | footer: "GitHub" 55 | footer_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 56 | ts: ts 57 | ] 58 | 59 | robot.send room: room, attachment 60 | -------------------------------------------------------------------------------- /lib/github-pull-request-review.coffee: -------------------------------------------------------------------------------- 1 | # Handle the GitHub "pull_request_review" event 2 | # 3 | # Usage: 4 | # 5 | # require('../lib/github-pull-request-review')(robot, room, data) 6 | # 7 | # OR 8 | # 9 | # github_pull_request_review = require '../lib/github-pull-request-review' 10 | # github_pull_request_review robot, room, data 11 | 12 | module.exports = (robot, room, payload) -> 13 | return if not payload.review? 14 | return if not payload.action? 15 | return if not payload.action in ["submitted", "dismissed"] 16 | 17 | state = payload.action 18 | state = "approved" if state == "submitted" and payload.review.state.match(/approved/i) 19 | state = "requested changes on" if state == "submitted" and payload.review.state.match(/pending/i) 20 | state = "commented on" if state == "submitted" and payload.review.state.match(/commented/i) 21 | content = payload.review.body 22 | 23 | repo = payload.repository.full_name 24 | user_name = payload.review.user.login 25 | user_url = payload.review.user.html_url 26 | pr_id = payload.pull_request.number 27 | pr_title = payload.pull_request.title 28 | pr_url = payload.pull_request.html_url 29 | 30 | ts = new Date payload.review.submitted_at 31 | ts = new Date ts.getTime() 32 | ts = Math.floor(ts.getTime() / 1000) 33 | 34 | attachment = 35 | attachments: [ 36 | fallback: "[#{repo}] #{user_name} #{state} pull request ##{pr_id}: #{pr_url}" 37 | color: "#E3E4E6" 38 | pretext: "[] <#{user_url}|#{user_name}> #{state} <#{pr_url}|##{pr_id} #{pr_title}>" 39 | author_name: "#{repo} (GitHub)" 40 | author_link: "https://github.com/#{repo}" 41 | author_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 42 | title: "Pull request review #{state} for #{repo}##{pr_id} #{pr_title}" 43 | title_link: pr_url 44 | text: content 45 | fields: [ 46 | { 47 | title: "Repository" 48 | value: "" 49 | short: true 50 | } 51 | { 52 | title: "Reviewer" 53 | value: "<#{user_url}|#{user_name}>" 54 | short: true 55 | } 56 | { 57 | title: "Status" 58 | value: state 59 | short: true 60 | } 61 | ] 62 | footer: "GitHub" 63 | footer_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 64 | ts: ts 65 | ] 66 | 67 | robot.send room: room, attachment 68 | -------------------------------------------------------------------------------- /lib/github-pull-request.coffee: -------------------------------------------------------------------------------- 1 | # Handle the GitHub "pull_request" event 2 | # 3 | # Usage: 4 | # 5 | # require('../lib/github-pull-request')(robot, room, data) 6 | # 7 | # OR 8 | # 9 | # github_pull_request = require '../lib/github-pull-request' 10 | # github_pull_request robot, room, data 11 | 12 | module.exports = (robot, room, payload) -> 13 | return if not payload.pull_request? 14 | return if not payload.action? 15 | return if not payload.action in ["opened", "closed", "reopened"] 16 | 17 | action = payload.action 18 | action = "merged" if action == "closed" and payload.pull_request.merged 19 | 20 | repo = payload.repository.full_name 21 | user_name = payload.sender.login 22 | user_url = payload.sender.html_url 23 | pr_id = payload.pull_request.number 24 | pr_title = payload.pull_request.title 25 | pr_url = payload.pull_request.html_url 26 | pr_content = if action == "created" then payload.pull_request.body else "" 27 | 28 | switch action 29 | when "opened" then ts = new Date payload.pull_request.created_at 30 | when "closed" then ts = new Date payload.pull_request.closed_at 31 | when "reopened" then ts = new Date payload.pull_request.updated_at 32 | when "merged" then ts = new Date payload.pull_request.merged_at 33 | else return 34 | 35 | ts = new Date ts.getTime() 36 | ts = Math.floor(ts.getTime() / 1000) 37 | 38 | attachment = 39 | attachments: [ 40 | fallback: "[#{repo}] Pull request #{action} by #{user_name}: #{pr_url}" 41 | color: "#E3E4E6" 42 | pretext: "[] Pull request #{action} by <#{user_url}|#{user_name}>" 43 | author_name: "#{repo} (GitHub)" 44 | author_link: "https://github.com/#{repo}" 45 | author_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 46 | title: "Pull request #{action}: #{repo}##{pr_id} #{pr_title}" 47 | title_link: pr_url 48 | text: pr_content 49 | fields: [ 50 | { 51 | title: "Repository" 52 | value: "" 53 | short: true 54 | } 55 | { 56 | title: "Reporter" 57 | value: "<#{user_url}|#{user_name}>" 58 | short: true 59 | } 60 | { 61 | title: "Status" 62 | value: action 63 | short: true 64 | } 65 | ] 66 | footer: "GitHub" 67 | footer_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 68 | ts: ts 69 | ] 70 | 71 | robot.send room: room, attachment 72 | -------------------------------------------------------------------------------- /lib/github-push.coffee: -------------------------------------------------------------------------------- 1 | # Un/Subscribe from/to Github PubSubHubbub events for a repository 2 | # 3 | # Simple class exposing two methods, subscribe and unsubscribe. Uses the 4 | # hubbaseurl to form the callback for each event, the provided secret 5 | # for specifying an encryption salt for verifying signatures of pushed 6 | # event payloads, and the OAuth2 personal access token to use with the 7 | # API. 8 | # 9 | # Usage: 10 | # 11 | # githubPush = require('../lib/github-push') 12 | # subscriptions = new githubPush( 13 | # robot, 14 | # "https://sub.example.com", 15 | # HUBOT_GITHUB_TOKEN, 16 | # HUBOT_GITHUB_SALT 17 | # ) 18 | # 19 | # subscriptions.subscribe(msg, "weierophinney/github_push") 20 | # subscriptions.unsubscribe(msg, "weierophinney/github_push") 21 | 22 | fetch = require 'node-fetch' 23 | formurlencoded = require 'form-urlencoded' 24 | crypto = require 'crypto' 25 | 26 | class GithubPush 27 | constructor: (@robot, @hubbaseurl, @token, @secret) -> 28 | 29 | BRAIN_GITHUB_REPOS: "github" 30 | PUSH_URI: "https://api.github.com/hub" 31 | 32 | events: [ 33 | "issues", 34 | "issue_comment", 35 | "pull_request", 36 | "pull_request_review", 37 | "pull_request_review_comment", 38 | "release", 39 | "status" 40 | ] 41 | 42 | subscribe: (msg, repo) -> 43 | room = msg.message.room 44 | @events.forEach (event) => 45 | data = 46 | "hub.mode": "subscribe" 47 | "hub.secret": @secret 48 | "hub.topic": "https://github.com/#{repo}/events/#{event}.json" 49 | "hub.callback": "#{@hubbaseurl}/#{room}/#{event}" 50 | fetch(@PUSH_URI, { 51 | method: "POST" 52 | body: formurlencoded(data) 53 | headers: 54 | Authorization: "token #{@token}" 55 | "Content-Type": "application/x-www-form-urlencoded" 56 | }).then((res) => 57 | if not res.ok 58 | res.text().then((resData) => 59 | msg.send "Error subscribing to #{repo} event #{event}; please check the logs" 60 | @robot.logger.error "Error subscribing to #{repo} event #{event} (#{res.status}): #{resData}" 61 | ) 62 | return 63 | 64 | repos = @robot.brain.get @BRAIN_GITHUB_REPOS 65 | repos = [] if not repos?.length? 66 | repos.push({ 67 | room: room 68 | repo: repo 69 | }) 70 | @robot.brain.set @BRAIN_GITHUB_REPOS, repos 71 | 72 | msg.send "Successfully subscribed to #{repo} #{event} event" 73 | ).catch((err) => 74 | msg.send "Error occurred while subscribing to #{repo} event #{event}; check the logs" 75 | @robot.logger.error "Error subscribing to #{repo} event #{event}: #{err}\n#{err.stack}" 76 | ); 77 | 78 | unsubscribe: (msg, repo) -> 79 | room = msg.message.room 80 | @events.forEach (event) => 81 | data = 82 | "hub.mode": "unsubscribe" 83 | "hub.topic": "https://github.com/#{repo}/events/#{event}.json" 84 | "hub.callback": "#{@hubbaseurl}/#{room}/#{event}" 85 | fetch(@PUSH_URI, { 86 | method: "POST" 87 | body: formurlencoded(data) 88 | headers: 89 | Authorization: "token #{@token}" 90 | "Content-Type": "application/x-www-form-urlencoded" 91 | }).then((res) => 92 | if not res.ok 93 | msg.send "Error unsubscribing to #{repo} event #{event}; please check the logs" 94 | @robot.logger.error "Error unsubscribing to #{repo} event #{event}: #{err}\n#{err.stack}" 95 | return 96 | 97 | repos = @robot.brain.get @BRAIN_GITHUB_REPOS 98 | repos = [] if not repos?.length? 99 | repos = repos.filter (compare) => 100 | compare.repo != repo and compare.room != room 101 | @robot.brain.set @BRAIN_GITHUB_REPOS, repos 102 | 103 | msg.send "Successfully unsubscribed from #{repo} event #{event}" 104 | ).catch((err) => 105 | msg.send "Error occurred while unsubscribing from #{repo} event #{event}; check the logs" 106 | @robot.logger.error "Error unsubscribing from #{repo} event #{event}: #{err}\n#{err.stack}" 107 | ); 108 | 109 | list: (msg) -> 110 | room = msg.message.room 111 | entries = @robot.brain.get @BRAIN_GITHUB_REPOS 112 | entries = [] if not entries?.length? 113 | entries = entries.filter (entry) -> entry.room == room 114 | 115 | reduce = (unique, entry) -> 116 | unique.push entry.repo if entry.repo not in unique 117 | unique 118 | 119 | repos = entries.reduce reduce, [] 120 | 121 | return msg.send "No github subscriptions in this room" if not repos.length 122 | 123 | repos.sort (a, b) -> a.localeCompare b, undefined, {numeric: true, sensitivity: "base"} 124 | repos = repos.map (repo) -> "- " 125 | 126 | repos.unshift "This room subscribes to the following github repositories:" 127 | msg.send repos.join("\n") 128 | 129 | clear: (msg) -> 130 | room = msg.message.room 131 | entries = @robot.brain.get @BRAIN_GITHUB_REPOS 132 | entries = [] if not entries?.length? 133 | entries.forEach (entry) => 134 | return if entry.room != room 135 | @unsubscribe msg, entry.repo 136 | 137 | verifySignature: (req) -> 138 | return false if not req.headers? 139 | return false if not req.headers.hasOwnProperty "x-hub-signature" 140 | 141 | header = req.headers["x-hub-signature"] 142 | signature = if header.match /^sha1\=/ then header.substring 5 else header 143 | 144 | compare = crypto.createHmac('sha1', @secret).update(req.rawBody, 'utf-8').digest('hex') 145 | return signature == compare.toString() 146 | 147 | verifyRoom: (repo, room) -> 148 | repos = @robot.brain.get @BRAIN_GITHUB_REPOS 149 | repos = [] if not repos?.length? 150 | repos = repos.filter (test) => 151 | return test.repo == repo and test.room == room 152 | return true if repos.length 153 | 154 | module.exports = GithubPush 155 | -------------------------------------------------------------------------------- /lib/github-release.coffee: -------------------------------------------------------------------------------- 1 | # Handle the GitHub "release" event 2 | # 3 | # Usage: 4 | # 5 | # require('../lib/github-release')(robot, room, data, callback_url, # callback_secret) 6 | # 7 | # OR 8 | # 9 | # github_release = require '../lib/github-release' 10 | # github_release robot, room, data, callback_url, callback_secret 11 | 12 | crypto = require 'crypto' 13 | 14 | module.exports = (robot, room, payload, callback_url, callback_secret) -> 15 | return if not payload.release? 16 | return if not payload.action? 17 | return if payload.action != "published" 18 | 19 | repo = payload.repository.full_name 20 | 21 | user_name = payload.release.author.login 22 | user_url = payload.release.author.html_url 23 | 24 | release_name = if payload.release.name != "" then payload.release.name else "#{repo} #{payload.release.tag_name}" 25 | release_url = payload.release.html_url 26 | release_body = payload.release.body 27 | 28 | ts = new Date payload.release.published_at 29 | ts = new Date ts.getTime() 30 | ts = Math.floor(ts.getTime() / 1000) 31 | 32 | attachment = 33 | attachments: [ 34 | fallback: "[#{repo}] New release #{release_name} created by #{user_name}: #{release_url}" 35 | color: "#4183C4" 36 | pretext: "[] New release <#{release_url}|#{release_name}> created by <#{user_url}|#{user_name}>" 37 | author_name: "#{repo} (GitHub)" 38 | author_link: "https://github.com/#{repo}" 39 | author_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 40 | title: release_name 41 | title_link: release_url 42 | text: release_body 43 | fields: [ 44 | { 45 | title: "Repository" 46 | value: "" 47 | short: true 48 | } 49 | { 50 | title: "Released By" 51 | value: "<#{user_url}|#{user_name}>" 52 | short: true 53 | } 54 | ] 55 | footer: "GitHub" 56 | footer_icon: "https://a.slack-edge.com/2fac/plugins/github/assets/service_36.png" 57 | ts: ts 58 | ] 59 | 60 | # Message the room 61 | robot.send room: room, attachment 62 | 63 | # Emit a tweet 64 | robot.emit "tweet", { 65 | status: "Released: #{release_name}\n\n #{release_url}" 66 | } 67 | 68 | # Notify the callback_url 69 | releasePayload = JSON.stringify payload 70 | signature = crypto.createHmac('sha1', callback_secret).update(releasePayload, 'utf-8').digest('hex') 71 | robot.http(callback_url) 72 | .header("X-Hub-Signature", "sha1=#{signature}") 73 | .post(releasePayload) (err, res, body) -> 74 | if err 75 | robot.logger.error "[GitHub Release] Error notifying #{callback_url} of release #{release_name}", err 76 | return 77 | if res.statusCode isnt 202 78 | robot.logger.error "[GitHub Release] Error returned by #{callback_url} for release #{release_name}: #{body}" 79 | -------------------------------------------------------------------------------- /lib/github-status.coffee: -------------------------------------------------------------------------------- 1 | # Handle the GitHub "status" event 2 | # 3 | # Only handles Travis-CI status at this time. If a pull request context is 4 | # detected, uses the sha1 passed in the event to search for the related pull 5 | # request via the GitHub API in order to display details about the origin of the 6 | # CI build. For normal pushes, simply indicates the build status for the 7 | # repository and the branch that was built. 8 | # 9 | # Usage: 10 | # 11 | # require('../lib/github-status')(robot, room, data, token) 12 | # 13 | # OR 14 | # 15 | # github_status = require '../lib/github-status' 16 | # github_status robot, room, data, token 17 | # 18 | # WHERE 19 | # 20 | # token is the OAuth2 personal access token to use when querying the API. 21 | # 22 | # TODO: 23 | # 24 | # The context for Travis is "continuous-integration/travis-ci/(pr|push)". 25 | # The segment at the end allows us to differentiate between failed builds due to 26 | # a pull request or a _push_ to a branch; this latter will allow us to trigger 27 | # documentation builds once we have a successful "push" build where 28 | # payload.branches[0].name == master. 29 | 30 | fetch = require 'node-fetch' 31 | 32 | module.exports = (robot, room, payload, token) -> 33 | return if not payload.state? 34 | return if not payload.state in ["success", "failure", "error"] 35 | return if not payload.context.match(/travis-ci/) 36 | 37 | travis_name = "Travis CI" 38 | travis_icon = "https://a.slack-edge.com/66f9/img/services/travis_36.png" 39 | travis_link = payload.target_url 40 | 41 | repo = payload.repository.full_name 42 | ts = new Date payload.updated_at 43 | ts = new Date ts.getTime() 44 | ts = Math.floor(ts.getTime() / 1000) 45 | 46 | switch payload.state 47 | when "success" 48 | status = "passed" 49 | color = "good" 50 | when "failure" 51 | status = "failed" 52 | color = "danger" 53 | when "error" 54 | status = "errored" 55 | color = "danger" 56 | else 57 | return 58 | 59 | if payload.context.match(/\/pr$/) 60 | query = encodeURIComponent("repo:#{repo}+#{payload.sha}") 61 | query = query.replace(/%20/, "+"); 62 | query = query.replace(/%2B/, "+"); 63 | url = "https://api.github.com/search/issues?q=#{query}" 64 | fetch(url, {headers: {Authorization: "token #{token}"}}) 65 | .then (res) => 66 | res.json() 67 | .then (search) => 68 | return if search.incomplete_results 69 | return if not search.items?.length? 70 | search.items.forEach (item) => 71 | return if not item.pull_request 72 | fetch(item.pull_request.url, {headers: {Authorization: "token #{token}"}}) 73 | .then (res) => 74 | res.json() 75 | .then (pr) => 76 | pr_id = pr.number 77 | pr_url = pr.html_url 78 | pr_title = pr.title 79 | 80 | attachment = 81 | attachments: [ 82 | fallback: "[#{repo}] Build #{status} for pull request ##{pr_id} #{pr_title}: #{travis_link}" 83 | color: color 84 | author_name: travis_name 85 | author_link: travis_link 86 | author_icon: travis_icon 87 | text: "<#{travis_link}|Build #{status}> for pull request <#{pr_url}|#{repo}##{pr_id} #{pr_title}>" 88 | fields: [ 89 | { 90 | title: "Repository" 91 | value: "" 92 | short: true 93 | } 94 | { 95 | title: "Status" 96 | value: status 97 | short: true 98 | } 99 | { 100 | title: "Pull Request" 101 | value: "<#{pr_url}|##{pr_id} #{pr_title}>" 102 | } 103 | ] 104 | footer: travis_name 105 | footer_icon: travis_icon 106 | ts: ts 107 | ] 108 | 109 | robot.send room: room, attachment 110 | 111 | .catch (err) => 112 | robot.logger.error "Error fetching pull request via #{item.pull_request.url}" 113 | .catch (err) => 114 | robot.logger.error "Error searching for status details using #{url}" 115 | return 116 | 117 | branch = payload.branches[0].name 118 | 119 | attachment = 120 | attachments: [ 121 | fallback: "Build #{status} for #{repo}@#{branch} (#{payload.sha.substring(0,8)}): #{travis_link}" 122 | color: color 123 | author_name: travis_name 124 | author_link: travis_link 125 | author_icon: travis_icon 126 | text: "<#{travis_link}|Build #{status}> for <#{payload.repository.html_url}|#{repo}>@#{branch} (<#{payload.commit.html_url}|#{payload.sha.substring(0, 8)}>)" 127 | fields: [ 128 | { 129 | title: "Repository" 130 | value: "" 131 | short: true 132 | } 133 | { 134 | title: "Status" 135 | value: status 136 | short: true 137 | } 138 | { 139 | title: "Branch" 140 | value: "#{branch} (#{payload.sha.substring(0,8)})" 141 | } 142 | ] 143 | footer: travis_name 144 | footer_icon: travis_icon 145 | ts: ts 146 | ] 147 | 148 | robot.send room: room, attachment 149 | 150 | robot.emit "build-success", {repo} if status == "passed" and branch == "master" 151 | -------------------------------------------------------------------------------- /lib/twitter-stream.coffee: -------------------------------------------------------------------------------- 1 | # Class representing a twitter stream being followed by hubot. 2 | 3 | TYPES = require "./twitter-types" 4 | 5 | class TwitterStream 6 | type: null 7 | room: null 8 | track: null 9 | follow: null 10 | screen_name: null 11 | tweet_stream: null 12 | 13 | toFollow: (room, follow, id) -> 14 | @type = TYPES.FOLLOW 15 | @screen_name = follow 16 | @follow = id 17 | @room = room 18 | 19 | toTrack: (room, track) -> 20 | @type = TYPES.TRACK 21 | @track = track 22 | @room = room 23 | 24 | module.exports = TwitterStream 25 | -------------------------------------------------------------------------------- /lib/twitter-tweeter.coffee: -------------------------------------------------------------------------------- 1 | # Send tweets 2 | 3 | fs = require "fs" 4 | 5 | class Tweeter 6 | constructor: (@robot, @twit) -> 7 | logoPath = __dirname + "/../img/zf-logo.png" 8 | @logo = fs.readFileSync logoPath, { encoding: "base64" } 9 | 10 | tweet: (tweet_data, callback) -> 11 | return if not tweet_data.status? 12 | 13 | @twit.post "media/upload", { media_data: @logo }, (err, data, res) => 14 | if err 15 | @robot.logger.error "[Tweeter] Error uploading ZF logo", err 16 | return 17 | 18 | metadata = 19 | media_id: data.media_id_string, 20 | alt_text: 21 | text: "Zend Framework" 22 | 23 | @twit.post "media/metadata/create", metadata, (err, data, res) => 24 | if err 25 | @robot.logger.error "[Tweeter] Error uploading ZF logo metadata", err 26 | return 27 | 28 | params = 29 | status: tweet_data.status 30 | media_ids: [ metadata.media_id ] 31 | 32 | @twit.post "statuses/update", params, (err, data, res) => 33 | if err 34 | @robot.logger.error "[Tweeter] Error posting status update", err 35 | return 36 | callback(data) if typeof(callback) == 'function' 37 | 38 | retweet: (id, callback) -> 39 | id = id.substr(id.lastIndexOf("/") + 1) if id.match /^https?:\/\/twitter.com/ 40 | @twit.post "statuses/retweet/:id", { id }, (err, data, res) => 41 | if err 42 | @robot.logger.error "[Tweeter] Error retweeting #{id}", err 43 | return 44 | callback(data) if typeof(callback) == 'function' 45 | 46 | module.exports = Tweeter 47 | -------------------------------------------------------------------------------- /lib/twitter-tweetstream.coffee: -------------------------------------------------------------------------------- 1 | # Interact with twitter streams 2 | # 3 | # Author: 4 | # Based on tweetstream by Christophe Hamerling 5 | # Rewritten in coffeescript and transformed into a class with authorization # guards by Matthew Weier O'Phinney 6 | 7 | _ = require('lodash') 8 | Stream = require './twitter-stream' 9 | TYPES = require './twitter-types' 10 | 11 | class TweetStream 12 | constructor: (@robot, @twit, @clear_subs) -> 13 | 14 | BRAIN_TWITTER_STREAMS: "twitter" 15 | loaded: false 16 | streams: [] 17 | 18 | saveTweetStream: (stream) -> 19 | @streams.push(stream) 20 | found = _.find @robot.brain.get(@BRAIN_TWITTER_STREAMS), (subscription) -> 21 | switch stream.type 22 | when TYPES.FOLLOW 23 | return subscription.follow == stream.follow && subscription.room == stream.room && subscription.type == stream.type 24 | when TYPES.TRACK 25 | return subscription.track == stream.track && subscription.room == stream.room && subscription.type == stream.type 26 | 27 | return if found 28 | 29 | toSave = null 30 | switch stream.type 31 | when TYPES.FOLLOW 32 | toSave = 33 | type: TYPES.FOLLOW 34 | room: stream.room 35 | follow: stream.follow 36 | screen_name: stream.screen_name 37 | when TYPES.TRACK 38 | toSave = 39 | type: TYPES.TRACK 40 | room: stream.room 41 | track: stream.track 42 | else 43 | return 44 | 45 | savedStreams = @robot.brain.get @BRAIN_TWITTER_STREAMS 46 | savedStreams.push toSave 47 | @robot.brain.set @BRAIN_TWITTER_STREAMS, savedStreams 48 | 49 | formatTimeString: (date) -> 50 | ampm = "am" 51 | hours = date.getHours() 52 | ampm = "pm" if hours > 11 53 | hours = hours - 12 if hours > 12 54 | hours = 12 if hours == 0 55 | minutes = date.getMinutes() 56 | minutes = "0" + minutes if minutes < 10 57 | 58 | "#{hours}:#{minutes} #{ampm}" 59 | 60 | initializeStream: (stream) -> 61 | filter = {} 62 | 63 | switch stream.type 64 | when TYPES.FOLLOW 65 | filter.follow = stream.follow 66 | when TYPES.TRACK 67 | filter.track = stream.track 68 | else 69 | return 70 | 71 | tweetStream = @twit.stream 'statuses/filter', filter 72 | tweetStream.on 'tweet', (tweet) => 73 | return if stream.type == TYPES.FOLLOW && tweet.user.id_str != stream.follow 74 | ts = new Date tweet.created_at 75 | ts = new Date ts.getTime() 76 | created = @formatTimeString ts 77 | 78 | attachment = 79 | attachments: [ 80 | color: "#00ACED" 81 | fallback: "@#{tweet.user.screen_name} at #{created}: https://twitter.com/#{tweet.user.screen_name}/status/#{tweet.id_str}" 82 | author_name: "#{tweet.user.screen_name} @#{tweet.user.name}" 83 | author_link: "https://twitter.com/#{tweet.user.screen_name}/status/#{tweet.id_str}" 84 | author_icon: "#{tweet.user.profile_image_url_https}" 85 | text: tweet.text 86 | footer: "Twitter" 87 | footer_icon: "https://a.slack-edge.com/66f9/img/services/twitter_128.png" 88 | ts: Math.floor(ts.getTime() / 1000) 89 | ] 90 | 91 | if tweet.entities?.media? && tweet.entities.media.length > 0 92 | tweet.entities.media.forEach (media) -> 93 | attachment.attachments.push { 94 | color: "#00ACED" 95 | fallback: media.media_url_https 96 | pretext: media.media_url_https 97 | image_url: media.media_url_https 98 | } 99 | 100 | @robot.send room: stream.room, attachment 101 | 102 | @robot.logger.info "Started a new twitter stream", filter 103 | stream.tweet_stream = tweetStream 104 | @saveTweetStream stream 105 | 106 | restoreSubscription: (subscription) -> 107 | return @robot.logger.error('Can not restore subscription; missing room or type', subscription) if !subscription || !subscription.room || !subscription.type 108 | 109 | switch subscription.type 110 | when TYPES.FOLLOW 111 | return @robot.logger.error('Can not restore follow subscription; missing follow identifier or screen name', subscription) if !subscription.screen_name || !subscription.follow 112 | stream = new Stream() 113 | stream.toFollow subscription.room, subscription.screen_name, subscription.follow 114 | @initializeStream stream 115 | when TYPES.TRACK 116 | return @robot.logger.error('Can not restore track subscription; missing tracking string', subscription) if !subscription.track 117 | stream = new Stream() 118 | stream.toTrack subscription.room, subscription.track 119 | @initializeStream stream 120 | 121 | restoreSubscriptions: -> 122 | subscriptions = @robot.brain.get @BRAIN_TWITTER_STREAMS 123 | return @robot.brain.set(@BRAIN_TWITTER_STREAMS, []) if !subscriptions?.length? 124 | @restoreSubscription(subscription) for subscription in subscriptions 125 | 126 | getIdFromScreenName: (screen_name, callback) -> 127 | @twit.get 'users/lookup', {screen_name}, (err, response) -> 128 | return callback(err) if err 129 | 130 | return callback(new Error("User not found")) if !response?.length? 131 | 132 | callback null, response[0].id_str 133 | 134 | load: (data) -> 135 | # this loaded event is sent on each robot.brain.set, skip it after initial load 136 | return if @loaded 137 | 138 | @loaded = true 139 | 140 | if @clear_subs then @robot.brain.set(@BRAIN_TWITTER_STREAMS, []) else @restoreSubscriptions() 141 | 142 | clear: (msg) -> 143 | match = (subscription) -> subscription.room == msg.message.room 144 | 145 | toRemove = _.remove @streams, match 146 | 147 | return msg.send "No subscription in this room" if !toRemove.length 148 | 149 | subscription.stream.stop() for subscription in toRemove 150 | 151 | savedStreams = @robot.brain.get @BRAIN_TWITTER_STREAMS 152 | @robot.brain.set(@BRAIN_TWITTER_STREAMS, _.remove(savedStreams, match)) 153 | 154 | msg.send "Unsubscribed from all" 155 | 156 | follow: (msg) -> 157 | screen_name = msg.match[1] 158 | @getIdFromScreenName screen_name, (err, id) => 159 | return @robot.logger.error("Can not get twitter user id from #{screen_name}", err) if err 160 | 161 | stream = new Stream() 162 | stream.toFollow msg.message.room, screen_name, id 163 | @initializeStream stream 164 | msg.send "I have started following tweets from @#{screen_name}" 165 | 166 | list: (msg) -> 167 | currentRoomTags = @streams 168 | .filter((subscription) -> subscription.room == msg.message.room) 169 | .map((subscription) -> 170 | if subscription.room == msg.message.room 171 | switch subscription.type 172 | when TYPES.FOLLOW then return "- From user @#{subscription.screen_name}" 173 | when TYPES.TRACK then return "- Matching #{subscription.track}" 174 | ) 175 | 176 | return msg.send("No subscriptions. Hint: Type 'twitter track/follow XXX' to listen to XXX related tweets in current room") if not currentRoomTags.length 177 | 178 | currentRoomTags.unshift "I am listening to tweets with the following criteria:" 179 | msg.send currentRoomTags.join "\n" 180 | 181 | unsubscribe: (match) -> 182 | toRemove = _.remove @streams, match 183 | return false if !toRemove.length 184 | 185 | subscription.tweet_stream.stop() for subscription in toRemove 186 | 187 | savedStreams = @robot.brain.get @BRAIN_TWITTER_STREAMS 188 | _.remove savedStreams, match 189 | @robot.brain.set @BRAIN_TWITTER_STREAMS, savedStreams 190 | true 191 | 192 | unfollow: (msg) -> 193 | screen_name = msg.match[1] 194 | 195 | msg.send("I stopped following tweets from '#{screen_name}'") if @unsubscribe((subscription) => subscription.type == TYPES.FOLLOW && subscription.screen_name == screen_name && subscription.room == msg.message.room) 196 | 197 | untrack: (msg) -> 198 | word = msg.match[1] 199 | msg.send("I have stopped tracking tweets matching '#{word}'") if @unsubscribe((subscription) => subscription.type == TYPES.TRACK && subscription.track == word && subscription.room == msg.message.room) 200 | 201 | track: (msg) -> 202 | stream = new Stream() 203 | stream.toTrack msg.message.room, msg.match[1] 204 | @initializeStream stream 205 | msg.send "I have started tracking tweets matching '#{msg.match[1]}'" 206 | 207 | module.exports = TweetStream 208 | -------------------------------------------------------------------------------- /lib/twitter-types.coffee: -------------------------------------------------------------------------------- 1 | # Types of twitter follows 2 | # 3 | # TRACK is to track a twitter search 4 | # FOLLOW is to follow a twitter user 5 | 6 | module.exports = 7 | TRACK: "track" 8 | FOLLOW: "follow" 9 | -------------------------------------------------------------------------------- /lib/zf-acl.coffee: -------------------------------------------------------------------------------- 1 | # Class for managing bot ACLs for the ZF slack 2 | 3 | _ = require "lodash" 4 | 5 | class ZfAcl 6 | constructor: (@robot, @user_whitelist = [], @verbose = false) -> 7 | 8 | BRAIN_ACL_WHITELIST: "zf-acl" 9 | loaded: false 10 | 11 | load: (data) -> 12 | # this loaded event is sent on each robot.brain.set, skip it after initial load 13 | return if @loaded 14 | 15 | @loaded = true 16 | @restoreAllowedUsers() 17 | 18 | restoreAllowedUsers: -> 19 | users = @robot.brain.get @BRAIN_ACL_WHITELIST 20 | console.log("Users", users) if @verbose 21 | console.log("No users found in brain!") if @verbose && !users?.length? 22 | return @robot.brain.set(@BRAIN_ACL_WHITELIST, @user_whitelist) if !users?.length? 23 | for user in users 24 | continue if user in @user_whitelist 25 | @user_whitelist.push(user) 26 | 27 | verify: (msg) -> 28 | return true if 1 > @user_whitelist.length 29 | msg.envelope.user.name in @user_whitelist 30 | 31 | allow: (msg) -> 32 | user = msg.match[1] 33 | return msg.send("User #{user} is already in the ACL whitelist.") if user in @user_whitelist 34 | 35 | @user_whitelist.push user 36 | @robot.brain.set @BRAIN_ACL_WHITELIST, @user_whitelist 37 | 38 | msg.send "User #{user} added to ACL whitelist." 39 | 40 | deny: (msg) -> 41 | user = msg.match[1] 42 | return msg.send "User #{user} is not in the ACL whitelist." if user not in @user_whitelist 43 | 44 | @user_whitelist = _.pull @user_whitelist, user 45 | @robot.brain.set @BRAIN_ACL_WHITELIST, @user_whitelist 46 | 47 | msg.send "User #{user} removed from ACL whitelist." 48 | 49 | list: (msg) -> 50 | if 0 == @user_whitelist.length 51 | msg.send "No users in whitelist!" 52 | return 53 | 54 | users = ["Found #{@user_whitelist.length} user#{if @user_whitelist.length > 1 then 's' else ''} in whitelist:"] 55 | @user_whitelist.forEach (user) -> users.push "- #{user}" 56 | msg.send users.join("\n") 57 | 58 | module.exports = ZfAcl 59 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zf-hubot", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@slack/client": { 8 | "version": "3.16.1-sec.2", 9 | "resolved": "https://registry.npmjs.org/@slack/client/-/client-3.16.1-sec.2.tgz", 10 | "integrity": "sha512-FAERJJAurczrvgShW+9V95NdVYgf8FdIexUqfBgNuToRrLRDRcWNf0nGyfKAvl2RH1ncppzJp3iXgMNJFnJXhQ==", 11 | "requires": { 12 | "async": "^1.5.0", 13 | "bluebird": "^3.3.3", 14 | "eventemitter3": "^1.1.1", 15 | "https-proxy-agent": "^2.2.0", 16 | "inherits": "^2.0.1", 17 | "lodash": "^4.13.1", 18 | "pkginfo": "^0.4.0", 19 | "request": "^2.85.0", 20 | "retry": "^0.9.0", 21 | "url-join": "0.0.1", 22 | "winston": "^2.1.1", 23 | "ws": "^1.0.1" 24 | }, 25 | "dependencies": { 26 | "assert-plus": { 27 | "version": "1.0.0", 28 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 29 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 30 | }, 31 | "async": { 32 | "version": "1.5.2", 33 | "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", 34 | "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" 35 | }, 36 | "aws-sign2": { 37 | "version": "0.7.0", 38 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 39 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 40 | }, 41 | "caseless": { 42 | "version": "0.12.0", 43 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 44 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 45 | }, 46 | "combined-stream": { 47 | "version": "1.0.7", 48 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", 49 | "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", 50 | "requires": { 51 | "delayed-stream": "~1.0.0" 52 | } 53 | }, 54 | "delayed-stream": { 55 | "version": "1.0.0", 56 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 57 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 58 | }, 59 | "forever-agent": { 60 | "version": "0.6.1", 61 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 62 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 63 | }, 64 | "form-data": { 65 | "version": "2.3.3", 66 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 67 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 68 | "requires": { 69 | "asynckit": "^0.4.0", 70 | "combined-stream": "^1.0.6", 71 | "mime-types": "^2.1.12" 72 | } 73 | }, 74 | "http-signature": { 75 | "version": "1.2.0", 76 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 77 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 78 | "requires": { 79 | "assert-plus": "^1.0.0", 80 | "jsprim": "^1.2.2", 81 | "sshpk": "^1.7.0" 82 | } 83 | }, 84 | "mime-db": { 85 | "version": "1.38.0", 86 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", 87 | "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" 88 | }, 89 | "mime-types": { 90 | "version": "2.1.22", 91 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", 92 | "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", 93 | "requires": { 94 | "mime-db": "~1.38.0" 95 | } 96 | }, 97 | "oauth-sign": { 98 | "version": "0.9.0", 99 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 100 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 101 | }, 102 | "punycode": { 103 | "version": "1.4.1", 104 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 105 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 106 | }, 107 | "qs": { 108 | "version": "6.5.2", 109 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 110 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 111 | }, 112 | "request": { 113 | "version": "2.88.0", 114 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 115 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 116 | "requires": { 117 | "aws-sign2": "~0.7.0", 118 | "aws4": "^1.8.0", 119 | "caseless": "~0.12.0", 120 | "combined-stream": "~1.0.6", 121 | "extend": "~3.0.2", 122 | "forever-agent": "~0.6.1", 123 | "form-data": "~2.3.2", 124 | "har-validator": "~5.1.0", 125 | "http-signature": "~1.2.0", 126 | "is-typedarray": "~1.0.0", 127 | "isstream": "~0.1.2", 128 | "json-stringify-safe": "~5.0.1", 129 | "mime-types": "~2.1.19", 130 | "oauth-sign": "~0.9.0", 131 | "performance-now": "^2.1.0", 132 | "qs": "~6.5.2", 133 | "safe-buffer": "^5.1.2", 134 | "tough-cookie": "~2.4.3", 135 | "tunnel-agent": "^0.6.0", 136 | "uuid": "^3.3.2" 137 | } 138 | }, 139 | "tough-cookie": { 140 | "version": "2.4.3", 141 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 142 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 143 | "requires": { 144 | "psl": "^1.1.24", 145 | "punycode": "^1.4.1" 146 | } 147 | }, 148 | "tunnel-agent": { 149 | "version": "0.6.0", 150 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 151 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 152 | "requires": { 153 | "safe-buffer": "^5.0.1" 154 | } 155 | }, 156 | "uuid": { 157 | "version": "3.3.2", 158 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 159 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 160 | } 161 | } 162 | }, 163 | "accepts": { 164 | "version": "1.2.13", 165 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz", 166 | "integrity": "sha1-5fHzkoxtlf2WVYw27D2dDeSm7Oo=", 167 | "requires": { 168 | "mime-types": "~2.1.6", 169 | "negotiator": "0.5.3" 170 | }, 171 | "dependencies": { 172 | "mime-db": { 173 | "version": "1.38.0", 174 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", 175 | "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" 176 | }, 177 | "mime-types": { 178 | "version": "2.1.22", 179 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", 180 | "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", 181 | "requires": { 182 | "mime-db": "~1.38.0" 183 | } 184 | } 185 | } 186 | }, 187 | "agent-base": { 188 | "version": "4.2.1", 189 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", 190 | "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", 191 | "requires": { 192 | "es6-promisify": "^5.0.0" 193 | } 194 | }, 195 | "ajv": { 196 | "version": "6.10.0", 197 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", 198 | "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", 199 | "requires": { 200 | "fast-deep-equal": "^2.0.1", 201 | "fast-json-stable-stringify": "^2.0.0", 202 | "json-schema-traverse": "^0.4.1", 203 | "uri-js": "^4.2.2" 204 | } 205 | }, 206 | "ansi-regex": { 207 | "version": "2.1.1", 208 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 209 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 210 | }, 211 | "ansi-styles": { 212 | "version": "2.2.1", 213 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", 214 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" 215 | }, 216 | "asap": { 217 | "version": "2.0.6", 218 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 219 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" 220 | }, 221 | "asn1": { 222 | "version": "0.1.11", 223 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.1.11.tgz", 224 | "integrity": "sha1-VZvhg3bQik7E2+gId9J4GGObLfc=", 225 | "optional": true 226 | }, 227 | "assert-plus": { 228 | "version": "0.1.5", 229 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz", 230 | "integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA=", 231 | "optional": true 232 | }, 233 | "async": { 234 | "version": "0.9.2", 235 | "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", 236 | "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" 237 | }, 238 | "asynckit": { 239 | "version": "0.4.0", 240 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 241 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 242 | }, 243 | "aws-sign2": { 244 | "version": "0.5.0", 245 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz", 246 | "integrity": "sha1-xXED96F/wDfwLXwuZLYC6iI/fWM=", 247 | "optional": true 248 | }, 249 | "aws4": { 250 | "version": "1.8.0", 251 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 252 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 253 | }, 254 | "balanced-match": { 255 | "version": "1.0.0", 256 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 257 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 258 | }, 259 | "base64-url": { 260 | "version": "1.2.1", 261 | "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.2.1.tgz", 262 | "integrity": "sha1-GZ/WYXAqDnt9yubgaYuwicUvbXg=" 263 | }, 264 | "basic-auth": { 265 | "version": "1.0.4", 266 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.0.4.tgz", 267 | "integrity": "sha1-Awk1sB3nyblKgksp8/zLdQ06UpA=" 268 | }, 269 | "basic-auth-connect": { 270 | "version": "1.0.0", 271 | "resolved": "https://registry.npmjs.org/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz", 272 | "integrity": "sha1-/bC0OWLKe0BFanwrtI/hc9otISI=" 273 | }, 274 | "batch": { 275 | "version": "0.5.3", 276 | "resolved": "https://registry.npmjs.org/batch/-/batch-0.5.3.tgz", 277 | "integrity": "sha1-PzQU84AyF0O/wQQvmoP/HVgk1GQ=" 278 | }, 279 | "bcrypt-pbkdf": { 280 | "version": "1.0.2", 281 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 282 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 283 | "requires": { 284 | "tweetnacl": "^0.14.3" 285 | } 286 | }, 287 | "bluebird": { 288 | "version": "3.5.4", 289 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", 290 | "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==" 291 | }, 292 | "body-parser": { 293 | "version": "1.13.3", 294 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.13.3.tgz", 295 | "integrity": "sha1-wIzzMMM1jhUQFqBXRvE/ApyX+pc=", 296 | "requires": { 297 | "bytes": "2.1.0", 298 | "content-type": "~1.0.1", 299 | "debug": "~2.2.0", 300 | "depd": "~1.0.1", 301 | "http-errors": "~1.3.1", 302 | "iconv-lite": "0.4.11", 303 | "on-finished": "~2.3.0", 304 | "qs": "4.0.0", 305 | "raw-body": "~2.1.2", 306 | "type-is": "~1.6.6" 307 | }, 308 | "dependencies": { 309 | "ee-first": { 310 | "version": "1.1.1", 311 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 312 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 313 | }, 314 | "mime-db": { 315 | "version": "1.38.0", 316 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", 317 | "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" 318 | }, 319 | "mime-types": { 320 | "version": "2.1.22", 321 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", 322 | "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", 323 | "requires": { 324 | "mime-db": "~1.38.0" 325 | } 326 | }, 327 | "on-finished": { 328 | "version": "2.3.0", 329 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 330 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 331 | "requires": { 332 | "ee-first": "1.1.1" 333 | } 334 | }, 335 | "qs": { 336 | "version": "4.0.0", 337 | "resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz", 338 | "integrity": "sha1-wx2bdOwn33XlQ6hseHKO2NRiNgc=" 339 | }, 340 | "type-is": { 341 | "version": "1.6.16", 342 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 343 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 344 | "requires": { 345 | "media-typer": "0.3.0", 346 | "mime-types": "~2.1.18" 347 | } 348 | } 349 | } 350 | }, 351 | "boom": { 352 | "version": "0.4.2", 353 | "resolved": "https://registry.npmjs.org/boom/-/boom-0.4.2.tgz", 354 | "integrity": "sha1-emNune1O/O+xnO9JR6PGffrukRs=", 355 | "optional": true, 356 | "requires": { 357 | "hoek": "0.9.x" 358 | } 359 | }, 360 | "brace-expansion": { 361 | "version": "1.1.11", 362 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 363 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 364 | "requires": { 365 | "balanced-match": "^1.0.0", 366 | "concat-map": "0.0.1" 367 | } 368 | }, 369 | "buffer-from": { 370 | "version": "1.1.1", 371 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 372 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 373 | }, 374 | "bytes": { 375 | "version": "2.1.0", 376 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.1.0.tgz", 377 | "integrity": "sha1-rJPEEOL/ycx89LRks4KJBn9eR7Q=" 378 | }, 379 | "caseless": { 380 | "version": "0.11.0", 381 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", 382 | "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=" 383 | }, 384 | "chalk": { 385 | "version": "1.1.3", 386 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", 387 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 388 | "requires": { 389 | "ansi-styles": "^2.2.1", 390 | "escape-string-regexp": "^1.0.2", 391 | "has-ansi": "^2.0.0", 392 | "strip-ansi": "^3.0.0", 393 | "supports-color": "^2.0.0" 394 | } 395 | }, 396 | "cline": { 397 | "version": "0.8.2", 398 | "resolved": "https://registry.npmjs.org/cline/-/cline-0.8.2.tgz", 399 | "integrity": "sha1-6RHnQaCtLiTSnm+rLPifoyLVnHY=" 400 | }, 401 | "coffee-script": { 402 | "version": "1.12.7", 403 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", 404 | "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==" 405 | }, 406 | "colors": { 407 | "version": "1.0.3", 408 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", 409 | "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" 410 | }, 411 | "combined-stream": { 412 | "version": "0.0.7", 413 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz", 414 | "integrity": "sha1-ATfmV7qlp1QcV6w3rF/AfXO03B8=", 415 | "optional": true, 416 | "requires": { 417 | "delayed-stream": "0.0.5" 418 | } 419 | }, 420 | "commander": { 421 | "version": "2.6.0", 422 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", 423 | "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=" 424 | }, 425 | "compressible": { 426 | "version": "2.0.16", 427 | "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.16.tgz", 428 | "integrity": "sha512-JQfEOdnI7dASwCuSPWIeVYwc/zMsu/+tRhoUvEfXz2gxOA2DNjmG5vhtFdBlhWPPGo+RdT9S3tgc/uH5qgDiiA==", 429 | "requires": { 430 | "mime-db": ">= 1.38.0 < 2" 431 | }, 432 | "dependencies": { 433 | "mime-db": { 434 | "version": "1.39.0", 435 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.39.0.tgz", 436 | "integrity": "sha512-DTsrw/iWVvwHH+9Otxccdyy0Tgiil6TWK/xhfARJZF/QFhwOgZgOIvA2/VIGpM8U7Q8z5nDmdDWC6tuVMJNibw==" 437 | } 438 | } 439 | }, 440 | "compression": { 441 | "version": "1.5.2", 442 | "resolved": "https://registry.npmjs.org/compression/-/compression-1.5.2.tgz", 443 | "integrity": "sha1-sDuNhub4rSloPLqN+R3cb/x3s5U=", 444 | "requires": { 445 | "accepts": "~1.2.12", 446 | "bytes": "2.1.0", 447 | "compressible": "~2.0.5", 448 | "debug": "~2.2.0", 449 | "on-headers": "~1.0.0", 450 | "vary": "~1.0.1" 451 | } 452 | }, 453 | "concat-map": { 454 | "version": "0.0.1", 455 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 456 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 457 | }, 458 | "concat-stream": { 459 | "version": "1.6.2", 460 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 461 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 462 | "requires": { 463 | "buffer-from": "^1.0.0", 464 | "inherits": "^2.0.3", 465 | "readable-stream": "^2.2.2", 466 | "typedarray": "^0.0.6" 467 | } 468 | }, 469 | "connect": { 470 | "version": "2.30.2", 471 | "resolved": "https://registry.npmjs.org/connect/-/connect-2.30.2.tgz", 472 | "integrity": "sha1-jam8vooFTT0xjXTf7JA7XDmhtgk=", 473 | "requires": { 474 | "basic-auth-connect": "1.0.0", 475 | "body-parser": "~1.13.3", 476 | "bytes": "2.1.0", 477 | "compression": "~1.5.2", 478 | "connect-timeout": "~1.6.2", 479 | "content-type": "~1.0.1", 480 | "cookie": "0.1.3", 481 | "cookie-parser": "~1.3.5", 482 | "cookie-signature": "1.0.6", 483 | "csurf": "~1.8.3", 484 | "debug": "~2.2.0", 485 | "depd": "~1.0.1", 486 | "errorhandler": "~1.4.2", 487 | "express-session": "~1.11.3", 488 | "finalhandler": "0.4.0", 489 | "fresh": "0.3.0", 490 | "http-errors": "~1.3.1", 491 | "method-override": "~2.3.5", 492 | "morgan": "~1.6.1", 493 | "multiparty": "3.3.2", 494 | "on-headers": "~1.0.0", 495 | "parseurl": "~1.3.0", 496 | "pause": "0.1.0", 497 | "qs": "4.0.0", 498 | "response-time": "~2.3.1", 499 | "serve-favicon": "~2.3.0", 500 | "serve-index": "~1.7.2", 501 | "serve-static": "~1.10.0", 502 | "type-is": "~1.6.6", 503 | "utils-merge": "1.0.0", 504 | "vhost": "~3.0.1" 505 | }, 506 | "dependencies": { 507 | "mime-db": { 508 | "version": "1.38.0", 509 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", 510 | "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" 511 | }, 512 | "mime-types": { 513 | "version": "2.1.22", 514 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", 515 | "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", 516 | "requires": { 517 | "mime-db": "~1.38.0" 518 | } 519 | }, 520 | "qs": { 521 | "version": "4.0.0", 522 | "resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz", 523 | "integrity": "sha1-wx2bdOwn33XlQ6hseHKO2NRiNgc=" 524 | }, 525 | "type-is": { 526 | "version": "1.6.16", 527 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 528 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 529 | "requires": { 530 | "media-typer": "0.3.0", 531 | "mime-types": "~2.1.18" 532 | } 533 | } 534 | } 535 | }, 536 | "connect-multiparty": { 537 | "version": "1.2.5", 538 | "resolved": "https://registry.npmjs.org/connect-multiparty/-/connect-multiparty-1.2.5.tgz", 539 | "integrity": "sha1-L6vs/cGop3S6GUhNzmYMgYqFVec=", 540 | "requires": { 541 | "multiparty": "~3.3.2", 542 | "on-finished": "~2.1.0", 543 | "qs": "~2.2.4", 544 | "type-is": "~1.5.2" 545 | }, 546 | "dependencies": { 547 | "qs": { 548 | "version": "2.2.5", 549 | "resolved": "https://registry.npmjs.org/qs/-/qs-2.2.5.tgz", 550 | "integrity": "sha1-EIirr53MCuWuRbcJ5sa1iIsjkjw=" 551 | } 552 | } 553 | }, 554 | "connect-timeout": { 555 | "version": "1.6.2", 556 | "resolved": "https://registry.npmjs.org/connect-timeout/-/connect-timeout-1.6.2.tgz", 557 | "integrity": "sha1-3ppexh4zoStu2qt7XwYumMWZuI4=", 558 | "requires": { 559 | "debug": "~2.2.0", 560 | "http-errors": "~1.3.1", 561 | "ms": "0.7.1", 562 | "on-headers": "~1.0.0" 563 | } 564 | }, 565 | "content-disposition": { 566 | "version": "0.5.0", 567 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz", 568 | "integrity": "sha1-QoT+auBjCHRjnkToCkGMKTQTXp4=" 569 | }, 570 | "content-type": { 571 | "version": "1.0.4", 572 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 573 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 574 | }, 575 | "cookie": { 576 | "version": "0.1.3", 577 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz", 578 | "integrity": "sha1-5zSlwUF/zkctWu+Cw4HKu2TRpDU=" 579 | }, 580 | "cookie-parser": { 581 | "version": "1.3.5", 582 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.3.5.tgz", 583 | "integrity": "sha1-nXVVcPtdF4kHcSJ6AjFNm+fPg1Y=", 584 | "requires": { 585 | "cookie": "0.1.3", 586 | "cookie-signature": "1.0.6" 587 | } 588 | }, 589 | "cookie-signature": { 590 | "version": "1.0.6", 591 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 592 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 593 | }, 594 | "core-util-is": { 595 | "version": "1.0.2", 596 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 597 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 598 | }, 599 | "crc": { 600 | "version": "3.3.0", 601 | "resolved": "https://registry.npmjs.org/crc/-/crc-3.3.0.tgz", 602 | "integrity": "sha1-+mIuG8OIvyVzCQgta2UgDOZwkLo=" 603 | }, 604 | "cryptiles": { 605 | "version": "0.2.2", 606 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.2.2.tgz", 607 | "integrity": "sha1-7ZH/HxetE9N0gohZT4pIoNJvMlw=", 608 | "optional": true, 609 | "requires": { 610 | "boom": "0.4.x" 611 | } 612 | }, 613 | "csrf": { 614 | "version": "3.0.6", 615 | "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.6.tgz", 616 | "integrity": "sha1-thEg3c7q/JHnbtUxO7XAsmZ7cQo=", 617 | "requires": { 618 | "rndm": "1.2.0", 619 | "tsscmp": "1.0.5", 620 | "uid-safe": "2.1.4" 621 | } 622 | }, 623 | "csurf": { 624 | "version": "1.8.3", 625 | "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.8.3.tgz", 626 | "integrity": "sha1-I/KhO/HY/OHQyZZYg5RELLqGpWo=", 627 | "requires": { 628 | "cookie": "0.1.3", 629 | "cookie-signature": "1.0.6", 630 | "csrf": "~3.0.0", 631 | "http-errors": "~1.3.1" 632 | } 633 | }, 634 | "ctype": { 635 | "version": "0.5.3", 636 | "resolved": "https://registry.npmjs.org/ctype/-/ctype-0.5.3.tgz", 637 | "integrity": "sha1-gsGMJGH3QRTvFsE1IkrQuRRMoS8=", 638 | "optional": true 639 | }, 640 | "cycle": { 641 | "version": "1.0.3", 642 | "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", 643 | "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" 644 | }, 645 | "dashdash": { 646 | "version": "1.14.1", 647 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 648 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 649 | "requires": { 650 | "assert-plus": "^1.0.0" 651 | }, 652 | "dependencies": { 653 | "assert-plus": { 654 | "version": "1.0.0", 655 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 656 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 657 | } 658 | } 659 | }, 660 | "debug": { 661 | "version": "2.2.0", 662 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", 663 | "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", 664 | "requires": { 665 | "ms": "0.7.1" 666 | } 667 | }, 668 | "delayed-stream": { 669 | "version": "0.0.5", 670 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz", 671 | "integrity": "sha1-1LH0OpPoKW3+AmlPRoC8N6MTxz8=", 672 | "optional": true 673 | }, 674 | "depd": { 675 | "version": "1.0.1", 676 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", 677 | "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo=" 678 | }, 679 | "destroy": { 680 | "version": "1.0.4", 681 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 682 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 683 | }, 684 | "discourse-api": { 685 | "version": "1.8.0", 686 | "resolved": "https://registry.npmjs.org/discourse-api/-/discourse-api-1.8.0.tgz", 687 | "integrity": "sha1-EIjhwlK3NllDcTXkyv2dlU5iKFQ=", 688 | "requires": { 689 | "querystring": "~0.2.0", 690 | "request": "~2.30.0", 691 | "sync-request": "^3.0.0" 692 | } 693 | }, 694 | "ecc-jsbn": { 695 | "version": "0.1.2", 696 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 697 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 698 | "requires": { 699 | "jsbn": "~0.1.0", 700 | "safer-buffer": "^2.1.0" 701 | } 702 | }, 703 | "ee-first": { 704 | "version": "1.1.0", 705 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", 706 | "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" 707 | }, 708 | "encoding": { 709 | "version": "0.1.12", 710 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", 711 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", 712 | "requires": { 713 | "iconv-lite": "~0.4.13" 714 | }, 715 | "dependencies": { 716 | "iconv-lite": { 717 | "version": "0.4.24", 718 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 719 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 720 | "requires": { 721 | "safer-buffer": ">= 2.1.2 < 3" 722 | } 723 | } 724 | } 725 | }, 726 | "errorhandler": { 727 | "version": "1.4.3", 728 | "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.4.3.tgz", 729 | "integrity": "sha1-t7cO2PNZ6duICS8tIMD4MUIK2D8=", 730 | "requires": { 731 | "accepts": "~1.3.0", 732 | "escape-html": "~1.0.3" 733 | }, 734 | "dependencies": { 735 | "accepts": { 736 | "version": "1.3.5", 737 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 738 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 739 | "requires": { 740 | "mime-types": "~2.1.18", 741 | "negotiator": "0.6.1" 742 | } 743 | }, 744 | "escape-html": { 745 | "version": "1.0.3", 746 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 747 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 748 | }, 749 | "mime-db": { 750 | "version": "1.38.0", 751 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", 752 | "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" 753 | }, 754 | "mime-types": { 755 | "version": "2.1.22", 756 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", 757 | "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", 758 | "requires": { 759 | "mime-db": "~1.38.0" 760 | } 761 | }, 762 | "negotiator": { 763 | "version": "0.6.1", 764 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 765 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 766 | } 767 | } 768 | }, 769 | "es6-promise": { 770 | "version": "4.2.6", 771 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz", 772 | "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==" 773 | }, 774 | "es6-promisify": { 775 | "version": "5.0.0", 776 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", 777 | "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", 778 | "requires": { 779 | "es6-promise": "^4.0.3" 780 | } 781 | }, 782 | "escape-html": { 783 | "version": "1.0.2", 784 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz", 785 | "integrity": "sha1-130y+pjjjC9BroXpJ44ODmuhAiw=" 786 | }, 787 | "escape-string-regexp": { 788 | "version": "1.0.5", 789 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 790 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 791 | }, 792 | "etag": { 793 | "version": "1.7.0", 794 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz", 795 | "integrity": "sha1-A9MLX2fdbmMtKUXTDWZScxo01dg=" 796 | }, 797 | "eventemitter3": { 798 | "version": "1.2.0", 799 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", 800 | "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=" 801 | }, 802 | "express": { 803 | "version": "3.21.2", 804 | "resolved": "https://registry.npmjs.org/express/-/express-3.21.2.tgz", 805 | "integrity": "sha1-DCkD7lxU5j1lqWFwdkcDVQZlo94=", 806 | "requires": { 807 | "basic-auth": "~1.0.3", 808 | "commander": "2.6.0", 809 | "connect": "2.30.2", 810 | "content-disposition": "0.5.0", 811 | "content-type": "~1.0.1", 812 | "cookie": "0.1.3", 813 | "cookie-signature": "1.0.6", 814 | "debug": "~2.2.0", 815 | "depd": "~1.0.1", 816 | "escape-html": "1.0.2", 817 | "etag": "~1.7.0", 818 | "fresh": "0.3.0", 819 | "merge-descriptors": "1.0.0", 820 | "methods": "~1.1.1", 821 | "mkdirp": "0.5.1", 822 | "parseurl": "~1.3.0", 823 | "proxy-addr": "~1.0.8", 824 | "range-parser": "~1.0.2", 825 | "send": "0.13.0", 826 | "utils-merge": "1.0.0", 827 | "vary": "~1.0.1" 828 | } 829 | }, 830 | "express-session": { 831 | "version": "1.11.3", 832 | "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.11.3.tgz", 833 | "integrity": "sha1-XMmPP1/4Ttg1+Ry/CqvQxxB0AK8=", 834 | "requires": { 835 | "cookie": "0.1.3", 836 | "cookie-signature": "1.0.6", 837 | "crc": "3.3.0", 838 | "debug": "~2.2.0", 839 | "depd": "~1.0.1", 840 | "on-headers": "~1.0.0", 841 | "parseurl": "~1.3.0", 842 | "uid-safe": "~2.0.0", 843 | "utils-merge": "1.0.0" 844 | }, 845 | "dependencies": { 846 | "uid-safe": { 847 | "version": "2.0.0", 848 | "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.0.0.tgz", 849 | "integrity": "sha1-p/PGymSh9qXQTsDvPkw9U2cxcTc=", 850 | "requires": { 851 | "base64-url": "1.2.1" 852 | } 853 | } 854 | } 855 | }, 856 | "extend": { 857 | "version": "3.0.2", 858 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 859 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 860 | }, 861 | "extsprintf": { 862 | "version": "1.3.0", 863 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 864 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 865 | }, 866 | "eyes": { 867 | "version": "0.1.8", 868 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 869 | "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" 870 | }, 871 | "fast-deep-equal": { 872 | "version": "2.0.1", 873 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", 874 | "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" 875 | }, 876 | "fast-json-stable-stringify": { 877 | "version": "2.0.0", 878 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 879 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 880 | }, 881 | "finalhandler": { 882 | "version": "0.4.0", 883 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz", 884 | "integrity": "sha1-llpS2ejQXSuFdUhUH7ibU6JJfZs=", 885 | "requires": { 886 | "debug": "~2.2.0", 887 | "escape-html": "1.0.2", 888 | "on-finished": "~2.3.0", 889 | "unpipe": "~1.0.0" 890 | }, 891 | "dependencies": { 892 | "ee-first": { 893 | "version": "1.1.1", 894 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 895 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 896 | }, 897 | "on-finished": { 898 | "version": "2.3.0", 899 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 900 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 901 | "requires": { 902 | "ee-first": "1.1.1" 903 | } 904 | } 905 | } 906 | }, 907 | "forever-agent": { 908 | "version": "0.5.2", 909 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.5.2.tgz", 910 | "integrity": "sha1-bQ4JxJIflKJ/Y9O0nF/v8epMUTA=" 911 | }, 912 | "form-data": { 913 | "version": "0.1.4", 914 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.1.4.tgz", 915 | "integrity": "sha1-kavXiKupcCsaq/qLwBAxoqyeOxI=", 916 | "optional": true, 917 | "requires": { 918 | "async": "~0.9.0", 919 | "combined-stream": "~0.0.4", 920 | "mime": "~1.2.11" 921 | } 922 | }, 923 | "form-urlencoded": { 924 | "version": "1.5.1", 925 | "resolved": "https://registry.npmjs.org/form-urlencoded/-/form-urlencoded-1.5.1.tgz", 926 | "integrity": "sha1-+2nRamRpNjs/2r+dJWo5p6XtLhM=" 927 | }, 928 | "forwarded": { 929 | "version": "0.1.2", 930 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 931 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 932 | }, 933 | "fresh": { 934 | "version": "0.3.0", 935 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz", 936 | "integrity": "sha1-ZR+DjiJCTnVm3hYdg1jKoZn4PU8=" 937 | }, 938 | "fs.realpath": { 939 | "version": "1.0.0", 940 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 941 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 942 | }, 943 | "getpass": { 944 | "version": "0.1.7", 945 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 946 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 947 | "requires": { 948 | "assert-plus": "^1.0.0" 949 | }, 950 | "dependencies": { 951 | "assert-plus": { 952 | "version": "1.0.0", 953 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 954 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 955 | } 956 | } 957 | }, 958 | "githubot": { 959 | "version": "1.0.1", 960 | "resolved": "https://registry.npmjs.org/githubot/-/githubot-1.0.1.tgz", 961 | "integrity": "sha1-V+TYIvpE/wUgWDhRi+8lYNxWQV0=", 962 | "requires": { 963 | "async": "0.2.x", 964 | "scoped-http-client": ">= 0.9.8" 965 | }, 966 | "dependencies": { 967 | "async": { 968 | "version": "0.2.10", 969 | "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", 970 | "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" 971 | } 972 | } 973 | }, 974 | "glob": { 975 | "version": "7.1.3", 976 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", 977 | "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", 978 | "requires": { 979 | "fs.realpath": "^1.0.0", 980 | "inflight": "^1.0.4", 981 | "inherits": "2", 982 | "minimatch": "^3.0.4", 983 | "once": "^1.3.0", 984 | "path-is-absolute": "^1.0.0" 985 | } 986 | }, 987 | "har-schema": { 988 | "version": "2.0.0", 989 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 990 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 991 | }, 992 | "har-validator": { 993 | "version": "5.1.3", 994 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 995 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 996 | "requires": { 997 | "ajv": "^6.5.5", 998 | "har-schema": "^2.0.0" 999 | } 1000 | }, 1001 | "has-ansi": { 1002 | "version": "2.0.0", 1003 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", 1004 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 1005 | "requires": { 1006 | "ansi-regex": "^2.0.0" 1007 | } 1008 | }, 1009 | "hawk": { 1010 | "version": "1.0.0", 1011 | "resolved": "https://registry.npmjs.org/hawk/-/hawk-1.0.0.tgz", 1012 | "integrity": "sha1-uQuxaYByhUEdp//LjdJZhQLTtS0=", 1013 | "optional": true, 1014 | "requires": { 1015 | "boom": "0.4.x", 1016 | "cryptiles": "0.2.x", 1017 | "hoek": "0.9.x", 1018 | "sntp": "0.2.x" 1019 | } 1020 | }, 1021 | "hoek": { 1022 | "version": "0.9.1", 1023 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.9.1.tgz", 1024 | "integrity": "sha1-PTIkYrrfB3Fup+uFuviAec3c5QU=", 1025 | "optional": true 1026 | }, 1027 | "html-entities": { 1028 | "version": "1.2.1", 1029 | "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", 1030 | "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" 1031 | }, 1032 | "htmlparser": { 1033 | "version": "1.7.7", 1034 | "resolved": "https://registry.npmjs.org/htmlparser/-/htmlparser-1.7.7.tgz", 1035 | "integrity": "sha1-GeezmX/2+6yZrlp9J2ZInv5+LQ4=" 1036 | }, 1037 | "http-basic": { 1038 | "version": "2.5.1", 1039 | "resolved": "https://registry.npmjs.org/http-basic/-/http-basic-2.5.1.tgz", 1040 | "integrity": "sha1-jORHvbW2xXf4pj4/p4BW7Eu02/s=", 1041 | "requires": { 1042 | "caseless": "~0.11.0", 1043 | "concat-stream": "^1.4.6", 1044 | "http-response-object": "^1.0.0" 1045 | } 1046 | }, 1047 | "http-errors": { 1048 | "version": "1.3.1", 1049 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz", 1050 | "integrity": "sha1-GX4izevUGYWF6GlO9nhhl7ke2UI=", 1051 | "requires": { 1052 | "inherits": "~2.0.1", 1053 | "statuses": "1" 1054 | } 1055 | }, 1056 | "http-response-object": { 1057 | "version": "1.1.0", 1058 | "resolved": "https://registry.npmjs.org/http-response-object/-/http-response-object-1.1.0.tgz", 1059 | "integrity": "sha1-p8TnWq6C87tJBOT0P2FWc7TVGMM=" 1060 | }, 1061 | "http-signature": { 1062 | "version": "0.10.1", 1063 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-0.10.1.tgz", 1064 | "integrity": "sha1-T72sEyVZqoMjEh5UB3nAoBKyfmY=", 1065 | "optional": true, 1066 | "requires": { 1067 | "asn1": "0.1.11", 1068 | "assert-plus": "^0.1.5", 1069 | "ctype": "0.5.3" 1070 | } 1071 | }, 1072 | "https-proxy-agent": { 1073 | "version": "2.2.1", 1074 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", 1075 | "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", 1076 | "requires": { 1077 | "agent-base": "^4.1.0", 1078 | "debug": "^3.1.0" 1079 | }, 1080 | "dependencies": { 1081 | "debug": { 1082 | "version": "3.2.6", 1083 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 1084 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 1085 | "requires": { 1086 | "ms": "^2.1.1" 1087 | } 1088 | }, 1089 | "ms": { 1090 | "version": "2.1.1", 1091 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 1092 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 1093 | } 1094 | } 1095 | }, 1096 | "hubot": { 1097 | "version": "2.19.0", 1098 | "resolved": "https://registry.npmjs.org/hubot/-/hubot-2.19.0.tgz", 1099 | "integrity": "sha1-h8Vy0hD7DV+J91YXeuACDUn/ujY=", 1100 | "requires": { 1101 | "async": ">=0.1.0 <1.0.0", 1102 | "chalk": "^1.0.0", 1103 | "cline": "^0.8.2", 1104 | "coffee-script": "1.6.3", 1105 | "connect-multiparty": "^1.2.5", 1106 | "express": "^3.21.2", 1107 | "log": "1.4.0", 1108 | "optparse": "1.0.4", 1109 | "scoped-http-client": "0.11.0" 1110 | }, 1111 | "dependencies": { 1112 | "coffee-script": { 1113 | "version": "1.6.3", 1114 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.6.3.tgz", 1115 | "integrity": "sha1-Y1XTLPGwTN/2tITl5xF4Ky8MOb4=" 1116 | } 1117 | } 1118 | }, 1119 | "hubot-diagnostics": { 1120 | "version": "0.0.1", 1121 | "resolved": "https://registry.npmjs.org/hubot-diagnostics/-/hubot-diagnostics-0.0.1.tgz", 1122 | "integrity": "sha1-aa6gGuTb3PBRISDE4z6WWt5VJiw=" 1123 | }, 1124 | "hubot-help": { 1125 | "version": "0.2.2", 1126 | "resolved": "https://registry.npmjs.org/hubot-help/-/hubot-help-0.2.2.tgz", 1127 | "integrity": "sha1-zqF+eCzndrdD9lOTk1s0MMLoGXw=" 1128 | }, 1129 | "hubot-maps": { 1130 | "version": "0.0.2", 1131 | "resolved": "https://registry.npmjs.org/hubot-maps/-/hubot-maps-0.0.2.tgz", 1132 | "integrity": "sha1-4nGbAQYeX9ozYxx5+ttrhX0fnjY=" 1133 | }, 1134 | "hubot-redis-brain": { 1135 | "version": "0.0.3", 1136 | "resolved": "https://registry.npmjs.org/hubot-redis-brain/-/hubot-redis-brain-0.0.3.tgz", 1137 | "integrity": "sha1-4vGuf4itZEm0UvXmbFg5KVsF9kQ=", 1138 | "requires": { 1139 | "redis": "0.8.4" 1140 | } 1141 | }, 1142 | "hubot-rules": { 1143 | "version": "0.1.2", 1144 | "resolved": "https://registry.npmjs.org/hubot-rules/-/hubot-rules-0.1.2.tgz", 1145 | "integrity": "sha1-jvMS2Lz0umaBExDatp9SjTRj9MU=" 1146 | }, 1147 | "hubot-scripts": { 1148 | "version": "2.17.2", 1149 | "resolved": "https://registry.npmjs.org/hubot-scripts/-/hubot-scripts-2.17.2.tgz", 1150 | "integrity": "sha1-mwjpB9XPq6cteDIEBnp4zp3zriQ=", 1151 | "requires": { 1152 | "redis": "0.8.4" 1153 | } 1154 | }, 1155 | "hubot-shipit": { 1156 | "version": "0.2.1", 1157 | "resolved": "https://registry.npmjs.org/hubot-shipit/-/hubot-shipit-0.2.1.tgz", 1158 | "integrity": "sha1-T7yeFhlTv3kB/cNuKnAsExA/FrI=" 1159 | }, 1160 | "hubot-slack": { 1161 | "version": "4.6.0", 1162 | "resolved": "https://registry.npmjs.org/hubot-slack/-/hubot-slack-4.6.0.tgz", 1163 | "integrity": "sha512-eYnOmn76kMOQzjPKKM6/woanhUG7EwhgviH34MNWkf2EhlkkouKSe0wTkIt4n9OPbvuDf+531PO+XeKXMs4nrA==", 1164 | "requires": { 1165 | "@slack/client": "3.16.1-sec.2", 1166 | "bluebird": "^3.5.1", 1167 | "lodash": "^4.17.10" 1168 | } 1169 | }, 1170 | "iconv-lite": { 1171 | "version": "0.4.11", 1172 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.11.tgz", 1173 | "integrity": "sha1-LstC/SlHRJIiCaLnxATayHk9it4=" 1174 | }, 1175 | "inflight": { 1176 | "version": "1.0.6", 1177 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1178 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 1179 | "requires": { 1180 | "once": "^1.3.0", 1181 | "wrappy": "1" 1182 | } 1183 | }, 1184 | "inherits": { 1185 | "version": "2.0.3", 1186 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 1187 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 1188 | }, 1189 | "ipaddr.js": { 1190 | "version": "1.0.5", 1191 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz", 1192 | "integrity": "sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c=" 1193 | }, 1194 | "is-stream": { 1195 | "version": "1.1.0", 1196 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 1197 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 1198 | }, 1199 | "is-typedarray": { 1200 | "version": "1.0.0", 1201 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 1202 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 1203 | }, 1204 | "isarray": { 1205 | "version": "1.0.0", 1206 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 1207 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 1208 | }, 1209 | "isstream": { 1210 | "version": "0.1.2", 1211 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 1212 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 1213 | }, 1214 | "jsbn": { 1215 | "version": "0.1.1", 1216 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 1217 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 1218 | }, 1219 | "json-schema": { 1220 | "version": "0.2.3", 1221 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 1222 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 1223 | }, 1224 | "json-schema-traverse": { 1225 | "version": "0.4.1", 1226 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1227 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 1228 | }, 1229 | "json-stringify-safe": { 1230 | "version": "5.0.1", 1231 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 1232 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 1233 | }, 1234 | "jsprim": { 1235 | "version": "1.4.1", 1236 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 1237 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 1238 | "requires": { 1239 | "assert-plus": "1.0.0", 1240 | "extsprintf": "1.3.0", 1241 | "json-schema": "0.2.3", 1242 | "verror": "1.10.0" 1243 | }, 1244 | "dependencies": { 1245 | "assert-plus": { 1246 | "version": "1.0.0", 1247 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 1248 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 1249 | } 1250 | } 1251 | }, 1252 | "lodash": { 1253 | "version": "4.17.11", 1254 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 1255 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" 1256 | }, 1257 | "log": { 1258 | "version": "1.4.0", 1259 | "resolved": "https://registry.npmjs.org/log/-/log-1.4.0.tgz", 1260 | "integrity": "sha1-S6HYkP3iSbAx3KA7w36q8yVlbxw=" 1261 | }, 1262 | "media-typer": { 1263 | "version": "0.3.0", 1264 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1265 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 1266 | }, 1267 | "merge-descriptors": { 1268 | "version": "1.0.0", 1269 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz", 1270 | "integrity": "sha1-IWnPdTjhsMyH+4jhUC2EdLv3mGQ=" 1271 | }, 1272 | "method-override": { 1273 | "version": "2.3.10", 1274 | "resolved": "https://registry.npmjs.org/method-override/-/method-override-2.3.10.tgz", 1275 | "integrity": "sha1-49r41d7hDdLc59SuiNYrvud0drQ=", 1276 | "requires": { 1277 | "debug": "2.6.9", 1278 | "methods": "~1.1.2", 1279 | "parseurl": "~1.3.2", 1280 | "vary": "~1.1.2" 1281 | }, 1282 | "dependencies": { 1283 | "debug": { 1284 | "version": "2.6.9", 1285 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1286 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1287 | "requires": { 1288 | "ms": "2.0.0" 1289 | } 1290 | }, 1291 | "ms": { 1292 | "version": "2.0.0", 1293 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1294 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1295 | }, 1296 | "vary": { 1297 | "version": "1.1.2", 1298 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1299 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1300 | } 1301 | } 1302 | }, 1303 | "methods": { 1304 | "version": "1.1.2", 1305 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1306 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 1307 | }, 1308 | "mime": { 1309 | "version": "1.2.11", 1310 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz", 1311 | "integrity": "sha1-WCA+7Ybjpe8XrtK32evUfwpg3RA=" 1312 | }, 1313 | "mime-db": { 1314 | "version": "1.12.0", 1315 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.12.0.tgz", 1316 | "integrity": "sha1-PQxjGA9FjrENMlqqN9fFiuMS6dc=" 1317 | }, 1318 | "mime-types": { 1319 | "version": "2.0.14", 1320 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.0.14.tgz", 1321 | "integrity": "sha1-MQ4VnbI+B3+Lsit0jav6SVcUCqY=", 1322 | "requires": { 1323 | "mime-db": "~1.12.0" 1324 | } 1325 | }, 1326 | "minimatch": { 1327 | "version": "3.0.4", 1328 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 1329 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 1330 | "requires": { 1331 | "brace-expansion": "^1.1.7" 1332 | } 1333 | }, 1334 | "minimist": { 1335 | "version": "0.0.8", 1336 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 1337 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 1338 | }, 1339 | "mkdirp": { 1340 | "version": "0.5.1", 1341 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 1342 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 1343 | "requires": { 1344 | "minimist": "0.0.8" 1345 | } 1346 | }, 1347 | "morgan": { 1348 | "version": "1.6.1", 1349 | "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.6.1.tgz", 1350 | "integrity": "sha1-X9gYOYxoGcuiinzWZk8pL+HAu/I=", 1351 | "requires": { 1352 | "basic-auth": "~1.0.3", 1353 | "debug": "~2.2.0", 1354 | "depd": "~1.0.1", 1355 | "on-finished": "~2.3.0", 1356 | "on-headers": "~1.0.0" 1357 | }, 1358 | "dependencies": { 1359 | "ee-first": { 1360 | "version": "1.1.1", 1361 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 1362 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 1363 | }, 1364 | "on-finished": { 1365 | "version": "2.3.0", 1366 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 1367 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 1368 | "requires": { 1369 | "ee-first": "1.1.1" 1370 | } 1371 | } 1372 | } 1373 | }, 1374 | "ms": { 1375 | "version": "0.7.1", 1376 | "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", 1377 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" 1378 | }, 1379 | "multiparty": { 1380 | "version": "3.3.2", 1381 | "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-3.3.2.tgz", 1382 | "integrity": "sha1-Nd5oBNwZZD5SSfPT473GyM4wHT8=", 1383 | "requires": { 1384 | "readable-stream": "~1.1.9", 1385 | "stream-counter": "~0.2.0" 1386 | }, 1387 | "dependencies": { 1388 | "isarray": { 1389 | "version": "0.0.1", 1390 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 1391 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 1392 | }, 1393 | "readable-stream": { 1394 | "version": "1.1.14", 1395 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 1396 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 1397 | "requires": { 1398 | "core-util-is": "~1.0.0", 1399 | "inherits": "~2.0.1", 1400 | "isarray": "0.0.1", 1401 | "string_decoder": "~0.10.x" 1402 | } 1403 | }, 1404 | "string_decoder": { 1405 | "version": "0.10.31", 1406 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 1407 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 1408 | } 1409 | } 1410 | }, 1411 | "negotiator": { 1412 | "version": "0.5.3", 1413 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz", 1414 | "integrity": "sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g=" 1415 | }, 1416 | "node-fetch": { 1417 | "version": "1.7.3", 1418 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", 1419 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", 1420 | "requires": { 1421 | "encoding": "^0.1.11", 1422 | "is-stream": "^1.0.1" 1423 | } 1424 | }, 1425 | "node-uuid": { 1426 | "version": "1.4.8", 1427 | "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", 1428 | "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=" 1429 | }, 1430 | "oauth-sign": { 1431 | "version": "0.3.0", 1432 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.3.0.tgz", 1433 | "integrity": "sha1-y1QPk7srIqfVlBaRoojWDo6pOG4=", 1434 | "optional": true 1435 | }, 1436 | "on-finished": { 1437 | "version": "2.1.1", 1438 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.1.1.tgz", 1439 | "integrity": "sha1-+CyhyeOk8yhrG5k4YQ5bhja9PLI=", 1440 | "requires": { 1441 | "ee-first": "1.1.0" 1442 | } 1443 | }, 1444 | "on-headers": { 1445 | "version": "1.0.2", 1446 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", 1447 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" 1448 | }, 1449 | "once": { 1450 | "version": "1.4.0", 1451 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1452 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1453 | "requires": { 1454 | "wrappy": "1" 1455 | } 1456 | }, 1457 | "options": { 1458 | "version": "0.0.6", 1459 | "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", 1460 | "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" 1461 | }, 1462 | "optparse": { 1463 | "version": "1.0.4", 1464 | "resolved": "https://registry.npmjs.org/optparse/-/optparse-1.0.4.tgz", 1465 | "integrity": "sha1-wGJXnS0F0kPCIaMEpx4Ml5YjzPE=" 1466 | }, 1467 | "parseurl": { 1468 | "version": "1.3.3", 1469 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1470 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 1471 | }, 1472 | "path-is-absolute": { 1473 | "version": "1.0.1", 1474 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1475 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 1476 | }, 1477 | "pause": { 1478 | "version": "0.1.0", 1479 | "resolved": "https://registry.npmjs.org/pause/-/pause-0.1.0.tgz", 1480 | "integrity": "sha1-68ikqGGf8LioGsFRPDQ0/0af23Q=" 1481 | }, 1482 | "performance-now": { 1483 | "version": "2.1.0", 1484 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 1485 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 1486 | }, 1487 | "pkginfo": { 1488 | "version": "0.4.1", 1489 | "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.1.tgz", 1490 | "integrity": "sha1-tUGO8EOd5UJfxJlQQtztFPsqhP8=" 1491 | }, 1492 | "process-nextick-args": { 1493 | "version": "2.0.0", 1494 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", 1495 | "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" 1496 | }, 1497 | "promise": { 1498 | "version": "7.3.1", 1499 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", 1500 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", 1501 | "requires": { 1502 | "asap": "~2.0.3" 1503 | } 1504 | }, 1505 | "proxy-addr": { 1506 | "version": "1.0.10", 1507 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", 1508 | "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=", 1509 | "requires": { 1510 | "forwarded": "~0.1.0", 1511 | "ipaddr.js": "1.0.5" 1512 | } 1513 | }, 1514 | "psl": { 1515 | "version": "1.1.31", 1516 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", 1517 | "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" 1518 | }, 1519 | "punycode": { 1520 | "version": "2.1.1", 1521 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1522 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 1523 | }, 1524 | "qs": { 1525 | "version": "0.6.6", 1526 | "resolved": "https://registry.npmjs.org/qs/-/qs-0.6.6.tgz", 1527 | "integrity": "sha1-bgFQmP9RlouKPIGQAdXyyJvEsQc=" 1528 | }, 1529 | "querystring": { 1530 | "version": "0.2.0", 1531 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 1532 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 1533 | }, 1534 | "random-bytes": { 1535 | "version": "1.0.0", 1536 | "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", 1537 | "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" 1538 | }, 1539 | "range-parser": { 1540 | "version": "1.0.3", 1541 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz", 1542 | "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU=" 1543 | }, 1544 | "raw-body": { 1545 | "version": "2.1.7", 1546 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.7.tgz", 1547 | "integrity": "sha1-rf6s4uT7MJgFgBTQjActzFl1h3Q=", 1548 | "requires": { 1549 | "bytes": "2.4.0", 1550 | "iconv-lite": "0.4.13", 1551 | "unpipe": "1.0.0" 1552 | }, 1553 | "dependencies": { 1554 | "bytes": { 1555 | "version": "2.4.0", 1556 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", 1557 | "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" 1558 | }, 1559 | "iconv-lite": { 1560 | "version": "0.4.13", 1561 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz", 1562 | "integrity": "sha1-H4irpKsLFQjoMSrMOTRfNumS4vI=" 1563 | } 1564 | } 1565 | }, 1566 | "readable-stream": { 1567 | "version": "2.3.6", 1568 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", 1569 | "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", 1570 | "requires": { 1571 | "core-util-is": "~1.0.0", 1572 | "inherits": "~2.0.3", 1573 | "isarray": "~1.0.0", 1574 | "process-nextick-args": "~2.0.0", 1575 | "safe-buffer": "~5.1.1", 1576 | "string_decoder": "~1.1.1", 1577 | "util-deprecate": "~1.0.1" 1578 | } 1579 | }, 1580 | "redis": { 1581 | "version": "0.8.4", 1582 | "resolved": "https://registry.npmjs.org/redis/-/redis-0.8.4.tgz", 1583 | "integrity": "sha1-FGCfJkFOIRwx480H3HmwS/n/GYA=" 1584 | }, 1585 | "request": { 1586 | "version": "2.30.0", 1587 | "resolved": "https://registry.npmjs.org/request/-/request-2.30.0.tgz", 1588 | "integrity": "sha1-jg028IBuiRFSSwcrZMXuU1oJ2GE=", 1589 | "requires": { 1590 | "aws-sign2": "~0.5.0", 1591 | "forever-agent": "~0.5.0", 1592 | "form-data": "~0.1.0", 1593 | "hawk": "~1.0.0", 1594 | "http-signature": "~0.10.0", 1595 | "json-stringify-safe": "~5.0.0", 1596 | "mime": "~1.2.9", 1597 | "node-uuid": "~1.4.0", 1598 | "oauth-sign": "~0.3.0", 1599 | "qs": "~0.6.0", 1600 | "tough-cookie": "~0.9.15", 1601 | "tunnel-agent": "~0.3.0" 1602 | } 1603 | }, 1604 | "response-time": { 1605 | "version": "2.3.2", 1606 | "resolved": "https://registry.npmjs.org/response-time/-/response-time-2.3.2.tgz", 1607 | "integrity": "sha1-/6cbq5UtYvfB1Jt0NDVfvGjf/Fo=", 1608 | "requires": { 1609 | "depd": "~1.1.0", 1610 | "on-headers": "~1.0.1" 1611 | }, 1612 | "dependencies": { 1613 | "depd": { 1614 | "version": "1.1.2", 1615 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 1616 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 1617 | } 1618 | } 1619 | }, 1620 | "retry": { 1621 | "version": "0.9.0", 1622 | "resolved": "https://registry.npmjs.org/retry/-/retry-0.9.0.tgz", 1623 | "integrity": "sha1-b2l+UKDk3cjI9/tUeptg3q1DZ40=" 1624 | }, 1625 | "rimraf": { 1626 | "version": "2.6.3", 1627 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", 1628 | "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", 1629 | "requires": { 1630 | "glob": "^7.1.3" 1631 | } 1632 | }, 1633 | "rndm": { 1634 | "version": "1.2.0", 1635 | "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", 1636 | "integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=" 1637 | }, 1638 | "safe-buffer": { 1639 | "version": "5.1.2", 1640 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1641 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1642 | }, 1643 | "safer-buffer": { 1644 | "version": "2.1.2", 1645 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1646 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1647 | }, 1648 | "scoped-http-client": { 1649 | "version": "0.11.0", 1650 | "resolved": "https://registry.npmjs.org/scoped-http-client/-/scoped-http-client-0.11.0.tgz", 1651 | "integrity": "sha1-iH+oKoNg8V1jmlLlBOVjwVfSbXQ=" 1652 | }, 1653 | "send": { 1654 | "version": "0.13.0", 1655 | "resolved": "https://registry.npmjs.org/send/-/send-0.13.0.tgz", 1656 | "integrity": "sha1-UY+SGusFYK7H3KspkLFM9vPM5d4=", 1657 | "requires": { 1658 | "debug": "~2.2.0", 1659 | "depd": "~1.0.1", 1660 | "destroy": "1.0.3", 1661 | "escape-html": "1.0.2", 1662 | "etag": "~1.7.0", 1663 | "fresh": "0.3.0", 1664 | "http-errors": "~1.3.1", 1665 | "mime": "1.3.4", 1666 | "ms": "0.7.1", 1667 | "on-finished": "~2.3.0", 1668 | "range-parser": "~1.0.2", 1669 | "statuses": "~1.2.1" 1670 | }, 1671 | "dependencies": { 1672 | "destroy": { 1673 | "version": "1.0.3", 1674 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz", 1675 | "integrity": "sha1-tDO0ck5x/YVR2YhRdIUcX8N34sk=" 1676 | }, 1677 | "ee-first": { 1678 | "version": "1.1.1", 1679 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 1680 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 1681 | }, 1682 | "mime": { 1683 | "version": "1.3.4", 1684 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", 1685 | "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" 1686 | }, 1687 | "on-finished": { 1688 | "version": "2.3.0", 1689 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 1690 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 1691 | "requires": { 1692 | "ee-first": "1.1.1" 1693 | } 1694 | }, 1695 | "statuses": { 1696 | "version": "1.2.1", 1697 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz", 1698 | "integrity": "sha1-3e1FzBglbVHtQK7BQkidXGECbSg=" 1699 | } 1700 | } 1701 | }, 1702 | "serve-favicon": { 1703 | "version": "2.3.2", 1704 | "resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.3.2.tgz", 1705 | "integrity": "sha1-3UGeJo3gEqtysxnTN/IQUBP5OB8=", 1706 | "requires": { 1707 | "etag": "~1.7.0", 1708 | "fresh": "0.3.0", 1709 | "ms": "0.7.2", 1710 | "parseurl": "~1.3.1" 1711 | }, 1712 | "dependencies": { 1713 | "ms": { 1714 | "version": "0.7.2", 1715 | "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", 1716 | "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=" 1717 | } 1718 | } 1719 | }, 1720 | "serve-index": { 1721 | "version": "1.7.3", 1722 | "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.7.3.tgz", 1723 | "integrity": "sha1-egV/xu4o3GP2RWbl+lexEahq7NI=", 1724 | "requires": { 1725 | "accepts": "~1.2.13", 1726 | "batch": "0.5.3", 1727 | "debug": "~2.2.0", 1728 | "escape-html": "~1.0.3", 1729 | "http-errors": "~1.3.1", 1730 | "mime-types": "~2.1.9", 1731 | "parseurl": "~1.3.1" 1732 | }, 1733 | "dependencies": { 1734 | "escape-html": { 1735 | "version": "1.0.3", 1736 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 1737 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 1738 | }, 1739 | "mime-db": { 1740 | "version": "1.38.0", 1741 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", 1742 | "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" 1743 | }, 1744 | "mime-types": { 1745 | "version": "2.1.22", 1746 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", 1747 | "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", 1748 | "requires": { 1749 | "mime-db": "~1.38.0" 1750 | } 1751 | } 1752 | } 1753 | }, 1754 | "serve-static": { 1755 | "version": "1.10.3", 1756 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.10.3.tgz", 1757 | "integrity": "sha1-zlpuzTEB/tXsCYJ9rCKpwpv7BTU=", 1758 | "requires": { 1759 | "escape-html": "~1.0.3", 1760 | "parseurl": "~1.3.1", 1761 | "send": "0.13.2" 1762 | }, 1763 | "dependencies": { 1764 | "depd": { 1765 | "version": "1.1.2", 1766 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 1767 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 1768 | }, 1769 | "ee-first": { 1770 | "version": "1.1.1", 1771 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 1772 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 1773 | }, 1774 | "escape-html": { 1775 | "version": "1.0.3", 1776 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 1777 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 1778 | }, 1779 | "mime": { 1780 | "version": "1.3.4", 1781 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", 1782 | "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" 1783 | }, 1784 | "on-finished": { 1785 | "version": "2.3.0", 1786 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 1787 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 1788 | "requires": { 1789 | "ee-first": "1.1.1" 1790 | } 1791 | }, 1792 | "send": { 1793 | "version": "0.13.2", 1794 | "resolved": "https://registry.npmjs.org/send/-/send-0.13.2.tgz", 1795 | "integrity": "sha1-dl52B8gFVFK7pvCwUllTUJhgNt4=", 1796 | "requires": { 1797 | "debug": "~2.2.0", 1798 | "depd": "~1.1.0", 1799 | "destroy": "~1.0.4", 1800 | "escape-html": "~1.0.3", 1801 | "etag": "~1.7.0", 1802 | "fresh": "0.3.0", 1803 | "http-errors": "~1.3.1", 1804 | "mime": "1.3.4", 1805 | "ms": "0.7.1", 1806 | "on-finished": "~2.3.0", 1807 | "range-parser": "~1.0.3", 1808 | "statuses": "~1.2.1" 1809 | } 1810 | }, 1811 | "statuses": { 1812 | "version": "1.2.1", 1813 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz", 1814 | "integrity": "sha1-3e1FzBglbVHtQK7BQkidXGECbSg=" 1815 | } 1816 | } 1817 | }, 1818 | "slackify-html": { 1819 | "version": "1.0.1", 1820 | "resolved": "https://registry.npmjs.org/slackify-html/-/slackify-html-1.0.1.tgz", 1821 | "integrity": "sha1-g6k2v7Sap0XD4eXm1tfIvu1Nvws=", 1822 | "requires": { 1823 | "html-entities": "^1.1.3", 1824 | "htmlparser": "^1.7.7" 1825 | } 1826 | }, 1827 | "sntp": { 1828 | "version": "0.2.4", 1829 | "resolved": "https://registry.npmjs.org/sntp/-/sntp-0.2.4.tgz", 1830 | "integrity": "sha1-+4hfGLDzqtGJ+CSGJTa87ux1CQA=", 1831 | "optional": true, 1832 | "requires": { 1833 | "hoek": "0.9.x" 1834 | } 1835 | }, 1836 | "sshpk": { 1837 | "version": "1.16.1", 1838 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 1839 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 1840 | "requires": { 1841 | "asn1": "~0.2.3", 1842 | "assert-plus": "^1.0.0", 1843 | "bcrypt-pbkdf": "^1.0.0", 1844 | "dashdash": "^1.12.0", 1845 | "ecc-jsbn": "~0.1.1", 1846 | "getpass": "^0.1.1", 1847 | "jsbn": "~0.1.0", 1848 | "safer-buffer": "^2.0.2", 1849 | "tweetnacl": "~0.14.0" 1850 | }, 1851 | "dependencies": { 1852 | "asn1": { 1853 | "version": "0.2.4", 1854 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 1855 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 1856 | "requires": { 1857 | "safer-buffer": "~2.1.0" 1858 | } 1859 | }, 1860 | "assert-plus": { 1861 | "version": "1.0.0", 1862 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 1863 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 1864 | } 1865 | } 1866 | }, 1867 | "stack-trace": { 1868 | "version": "0.0.10", 1869 | "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", 1870 | "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" 1871 | }, 1872 | "statuses": { 1873 | "version": "1.5.0", 1874 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1875 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1876 | }, 1877 | "stream-counter": { 1878 | "version": "0.2.0", 1879 | "resolved": "https://registry.npmjs.org/stream-counter/-/stream-counter-0.2.0.tgz", 1880 | "integrity": "sha1-3tJmVWMZyLDiIoErnPOyb6fZR94=", 1881 | "requires": { 1882 | "readable-stream": "~1.1.8" 1883 | }, 1884 | "dependencies": { 1885 | "isarray": { 1886 | "version": "0.0.1", 1887 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 1888 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 1889 | }, 1890 | "readable-stream": { 1891 | "version": "1.1.14", 1892 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 1893 | "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", 1894 | "requires": { 1895 | "core-util-is": "~1.0.0", 1896 | "inherits": "~2.0.1", 1897 | "isarray": "0.0.1", 1898 | "string_decoder": "~0.10.x" 1899 | } 1900 | }, 1901 | "string_decoder": { 1902 | "version": "0.10.31", 1903 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", 1904 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" 1905 | } 1906 | } 1907 | }, 1908 | "string_decoder": { 1909 | "version": "1.1.1", 1910 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1911 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1912 | "requires": { 1913 | "safe-buffer": "~5.1.0" 1914 | } 1915 | }, 1916 | "strip-ansi": { 1917 | "version": "3.0.1", 1918 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 1919 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 1920 | "requires": { 1921 | "ansi-regex": "^2.0.0" 1922 | } 1923 | }, 1924 | "supports-color": { 1925 | "version": "2.0.0", 1926 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", 1927 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" 1928 | }, 1929 | "sync-request": { 1930 | "version": "3.0.1", 1931 | "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-3.0.1.tgz", 1932 | "integrity": "sha1-yqEjWq+Im6UBB2oYNMQ2gwqC+3M=", 1933 | "requires": { 1934 | "concat-stream": "^1.4.7", 1935 | "http-response-object": "^1.0.1", 1936 | "then-request": "^2.0.1" 1937 | } 1938 | }, 1939 | "then-request": { 1940 | "version": "2.2.0", 1941 | "resolved": "https://registry.npmjs.org/then-request/-/then-request-2.2.0.tgz", 1942 | "integrity": "sha1-ZnizL6DKIY/laZgbvYhxtZQGDYE=", 1943 | "requires": { 1944 | "caseless": "~0.11.0", 1945 | "concat-stream": "^1.4.7", 1946 | "http-basic": "^2.5.1", 1947 | "http-response-object": "^1.1.0", 1948 | "promise": "^7.1.1", 1949 | "qs": "^6.1.0" 1950 | }, 1951 | "dependencies": { 1952 | "qs": { 1953 | "version": "6.7.0", 1954 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 1955 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 1956 | } 1957 | } 1958 | }, 1959 | "tough-cookie": { 1960 | "version": "0.9.15", 1961 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-0.9.15.tgz", 1962 | "integrity": "sha1-dWF6w0fjZZBSsDUBMYhYKWdzmfY=", 1963 | "optional": true, 1964 | "requires": { 1965 | "punycode": ">=0.2.0" 1966 | } 1967 | }, 1968 | "tsscmp": { 1969 | "version": "1.0.5", 1970 | "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz", 1971 | "integrity": "sha1-fcSjOvcVgatDN9qR2FylQn69mpc=" 1972 | }, 1973 | "tunnel-agent": { 1974 | "version": "0.3.0", 1975 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.3.0.tgz", 1976 | "integrity": "sha1-rWgbaPUyGtKCfEz7G31d8s/pQu4=", 1977 | "optional": true 1978 | }, 1979 | "tweetnacl": { 1980 | "version": "0.14.5", 1981 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1982 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 1983 | }, 1984 | "twit": { 1985 | "version": "2.2.11", 1986 | "resolved": "https://registry.npmjs.org/twit/-/twit-2.2.11.tgz", 1987 | "integrity": "sha512-BkdwvZGRVoUTcEBp0zuocuqfih4LB+kEFUWkWJOVBg6pAE9Ebv9vmsYTTrfXleZGf45Bj5H3A1/O9YhF2uSYNg==", 1988 | "requires": { 1989 | "bluebird": "^3.1.5", 1990 | "mime": "^1.3.4", 1991 | "request": "^2.68.0" 1992 | }, 1993 | "dependencies": { 1994 | "assert-plus": { 1995 | "version": "1.0.0", 1996 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 1997 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 1998 | }, 1999 | "aws-sign2": { 2000 | "version": "0.7.0", 2001 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 2002 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 2003 | }, 2004 | "caseless": { 2005 | "version": "0.12.0", 2006 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 2007 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 2008 | }, 2009 | "combined-stream": { 2010 | "version": "1.0.7", 2011 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", 2012 | "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", 2013 | "requires": { 2014 | "delayed-stream": "~1.0.0" 2015 | } 2016 | }, 2017 | "delayed-stream": { 2018 | "version": "1.0.0", 2019 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 2020 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 2021 | }, 2022 | "forever-agent": { 2023 | "version": "0.6.1", 2024 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 2025 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 2026 | }, 2027 | "form-data": { 2028 | "version": "2.3.3", 2029 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 2030 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 2031 | "requires": { 2032 | "asynckit": "^0.4.0", 2033 | "combined-stream": "^1.0.6", 2034 | "mime-types": "^2.1.12" 2035 | } 2036 | }, 2037 | "http-signature": { 2038 | "version": "1.2.0", 2039 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 2040 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 2041 | "requires": { 2042 | "assert-plus": "^1.0.0", 2043 | "jsprim": "^1.2.2", 2044 | "sshpk": "^1.7.0" 2045 | } 2046 | }, 2047 | "mime": { 2048 | "version": "1.6.0", 2049 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 2050 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 2051 | }, 2052 | "mime-db": { 2053 | "version": "1.38.0", 2054 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", 2055 | "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" 2056 | }, 2057 | "mime-types": { 2058 | "version": "2.1.22", 2059 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", 2060 | "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", 2061 | "requires": { 2062 | "mime-db": "~1.38.0" 2063 | } 2064 | }, 2065 | "oauth-sign": { 2066 | "version": "0.9.0", 2067 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 2068 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 2069 | }, 2070 | "punycode": { 2071 | "version": "1.4.1", 2072 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 2073 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 2074 | }, 2075 | "qs": { 2076 | "version": "6.5.2", 2077 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 2078 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 2079 | }, 2080 | "request": { 2081 | "version": "2.88.0", 2082 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 2083 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 2084 | "requires": { 2085 | "aws-sign2": "~0.7.0", 2086 | "aws4": "^1.8.0", 2087 | "caseless": "~0.12.0", 2088 | "combined-stream": "~1.0.6", 2089 | "extend": "~3.0.2", 2090 | "forever-agent": "~0.6.1", 2091 | "form-data": "~2.3.2", 2092 | "har-validator": "~5.1.0", 2093 | "http-signature": "~1.2.0", 2094 | "is-typedarray": "~1.0.0", 2095 | "isstream": "~0.1.2", 2096 | "json-stringify-safe": "~5.0.1", 2097 | "mime-types": "~2.1.19", 2098 | "oauth-sign": "~0.9.0", 2099 | "performance-now": "^2.1.0", 2100 | "qs": "~6.5.2", 2101 | "safe-buffer": "^5.1.2", 2102 | "tough-cookie": "~2.4.3", 2103 | "tunnel-agent": "^0.6.0", 2104 | "uuid": "^3.3.2" 2105 | } 2106 | }, 2107 | "tough-cookie": { 2108 | "version": "2.4.3", 2109 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 2110 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 2111 | "requires": { 2112 | "psl": "^1.1.24", 2113 | "punycode": "^1.4.1" 2114 | } 2115 | }, 2116 | "tunnel-agent": { 2117 | "version": "0.6.0", 2118 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 2119 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 2120 | "requires": { 2121 | "safe-buffer": "^5.0.1" 2122 | } 2123 | }, 2124 | "uuid": { 2125 | "version": "3.3.2", 2126 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 2127 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 2128 | } 2129 | } 2130 | }, 2131 | "type-is": { 2132 | "version": "1.5.7", 2133 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.5.7.tgz", 2134 | "integrity": "sha1-uTaKWTzG730GReeLL0xky+zQXpA=", 2135 | "requires": { 2136 | "media-typer": "0.3.0", 2137 | "mime-types": "~2.0.9" 2138 | } 2139 | }, 2140 | "typedarray": { 2141 | "version": "0.0.6", 2142 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 2143 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" 2144 | }, 2145 | "uid-safe": { 2146 | "version": "2.1.4", 2147 | "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.4.tgz", 2148 | "integrity": "sha1-Otbzg2jG1MjHXsF2I/t5qh0HHYE=", 2149 | "requires": { 2150 | "random-bytes": "~1.0.0" 2151 | } 2152 | }, 2153 | "ultron": { 2154 | "version": "1.0.2", 2155 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", 2156 | "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=" 2157 | }, 2158 | "unpipe": { 2159 | "version": "1.0.0", 2160 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 2161 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 2162 | }, 2163 | "uri-js": { 2164 | "version": "4.2.2", 2165 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 2166 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 2167 | "requires": { 2168 | "punycode": "^2.1.0" 2169 | } 2170 | }, 2171 | "url-join": { 2172 | "version": "0.0.1", 2173 | "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", 2174 | "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=" 2175 | }, 2176 | "util-deprecate": { 2177 | "version": "1.0.2", 2178 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 2179 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 2180 | }, 2181 | "utils-merge": { 2182 | "version": "1.0.0", 2183 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", 2184 | "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" 2185 | }, 2186 | "vary": { 2187 | "version": "1.0.1", 2188 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz", 2189 | "integrity": "sha1-meSYFWaihhGN+yuBc1ffeZM3bRA=" 2190 | }, 2191 | "verror": { 2192 | "version": "1.10.0", 2193 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 2194 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 2195 | "requires": { 2196 | "assert-plus": "^1.0.0", 2197 | "core-util-is": "1.0.2", 2198 | "extsprintf": "^1.2.0" 2199 | }, 2200 | "dependencies": { 2201 | "assert-plus": { 2202 | "version": "1.0.0", 2203 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 2204 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 2205 | } 2206 | } 2207 | }, 2208 | "vhost": { 2209 | "version": "3.0.2", 2210 | "resolved": "https://registry.npmjs.org/vhost/-/vhost-3.0.2.tgz", 2211 | "integrity": "sha1-L7HezUxGaqiLD5NBrzPcGv8keNU=" 2212 | }, 2213 | "winston": { 2214 | "version": "2.4.4", 2215 | "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.4.tgz", 2216 | "integrity": "sha512-NBo2Pepn4hK4V01UfcWcDlmiVTs7VTB1h7bgnB0rgP146bYhMxX0ypCz3lBOfNxCO4Zuek7yeT+y/zM1OfMw4Q==", 2217 | "requires": { 2218 | "async": "~1.0.0", 2219 | "colors": "1.0.x", 2220 | "cycle": "1.0.x", 2221 | "eyes": "0.1.x", 2222 | "isstream": "0.1.x", 2223 | "stack-trace": "0.0.x" 2224 | }, 2225 | "dependencies": { 2226 | "async": { 2227 | "version": "1.0.0", 2228 | "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", 2229 | "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" 2230 | } 2231 | } 2232 | }, 2233 | "wrappy": { 2234 | "version": "1.0.2", 2235 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2236 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 2237 | }, 2238 | "ws": { 2239 | "version": "1.1.5", 2240 | "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", 2241 | "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", 2242 | "requires": { 2243 | "options": ">=0.0.5", 2244 | "ultron": "1.0.x" 2245 | } 2246 | } 2247 | } 2248 | } 2249 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zf-hubot", 3 | "version": "0.0.0", 4 | "private": true, 5 | "author": "Matthew O'Phinney ", 6 | "description": "Chatbot/integrations for Zend Framework Slack", 7 | "dependencies": { 8 | "coffee-script": "^1.12.6", 9 | "discourse-api": "^1.8.0", 10 | "form-urlencoded": "^1.5.1", 11 | "githubot": "^1.0.1", 12 | "hubot": "^2.19.0", 13 | "hubot-diagnostics": "0.0.1", 14 | "hubot-help": "^0.2.0", 15 | "hubot-maps": "0.0.2", 16 | "hubot-redis-brain": "0.0.3", 17 | "hubot-rules": "^0.1.1", 18 | "hubot-scripts": "^2.17.2", 19 | "hubot-shipit": "^0.2.0", 20 | "hubot-slack": "^4.6.0", 21 | "lodash": "^4.17.11", 22 | "node-fetch": "^1.7.1", 23 | "rimraf": "^2.6.3", 24 | "slackify-html": "^1.0.1", 25 | "twit": "^2.2.11" 26 | }, 27 | "engines": { 28 | "node": "0.10.x" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/catch-all.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Catch all unhandled messages. 3 | 4 | module.exports = (robot) -> 5 | robot.catchAll (msg) -> 6 | return if not msg.message.text? 7 | 8 | regexp = new RegExp "^(@?#{robot.alias}:?|#{robot.name})", "i" 9 | matches = msg.message.text.match regexp 10 | 11 | return if matches == null or matches.length == 0 12 | 13 | msg.reply "I am unable to comply." 14 | msg.finish() 15 | -------------------------------------------------------------------------------- /scripts/discourse.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Listen to Discourse webhooks. Listens for "topic" and "post" hooks, passing 3 | # them on to dedicated webhook scripts. Middleware is registered that verifies 4 | # the incoming payload's X-Discourse-Event-Signature against a known secret to 5 | # validate the origin of the webhook payload. 6 | # 7 | # Register webhooks via the Discourse settings UI. When you do, use URIs of 8 | # the format "/discourse/{ROOM_ID}/(topic|post)", where {ROOM_ID} is the 9 | # identifier for the Slack room to which the webhook should post messages (you 10 | # can retrieve that by right-clicking a room, copying the URL, and extracting 11 | # the final path segment). 12 | # 13 | # Configuration: 14 | # 15 | # The following environment variables are required. 16 | # 17 | # HUBOT_DISCOURSE_URL: Base URL to the Discourse installation 18 | # HUBOT_DISCOURSE_SECRET: Shared secret between Discourse instance and webhook # (for verifying signatures) 19 | # 20 | # Author: 21 | # Matthew Weier O'Phinney 22 | 23 | bodyParser = require 'body-parser' 24 | discourse_post = require "../lib/discourse-post" 25 | discourse_topic = require "../lib/discourse-topic" 26 | verify_signature = require "../lib/discourse-verify-signature" 27 | 28 | module.exports = (robot) -> 29 | 30 | discourse_url = process.env.HUBOT_DISCOURSE_URL 31 | discourse_secret = process.env.HUBOT_DISCOURSE_SECRET 32 | 33 | # In order to calculate signatures, we need to shove the JSON body 34 | # parser to the top of the stack and have it set the raw request body 35 | # contents in the request when done parsing. 36 | robot.router.stack.unshift { 37 | route: "/discourse" 38 | handle: bodyParser.json { 39 | verify: (req, res, buf, encoding) -> 40 | req.rawBody = buf 41 | } 42 | } 43 | 44 | robot.router.post '/discourse/:room/:event', (req, res) -> 45 | room = req.params.room 46 | event = req.params.event 47 | 48 | if event not in ["topic", "post"] 49 | res.send 203, "Unrecognized event #{event}" 50 | robot.logger.error "[Discourse] Unrecognized event '#{event}' was pinged" 51 | return 52 | 53 | if not verify_signature(req, discourse_secret) 54 | res.send 203, "Invalid or missing signature" 55 | robot.logger.error "Invalid payload submitted to /discourse/#{room}/#{event}; signature invalid" 56 | return 57 | 58 | # We can accept it now, so return a response immediately 59 | res.send 202 60 | 61 | data = req.body 62 | 63 | # Now, we need to switch on the event, and determine what message to send 64 | # to the room. 65 | switch event 66 | # Uncomment to enable broadcast of topic created/edited events 67 | # when "topic" 68 | # discourse_topic robot, room, discourse_url, data 69 | when "post" 70 | discourse_post robot, room, discourse_url, data 71 | -------------------------------------------------------------------------------- /scripts/docs.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Documentation build automation 3 | # 4 | # Commands: 5 | # hubot docs build - Start building docs for the given repository 6 | # 7 | # Configuration: 8 | # 9 | # The following environment variables are required. You will need to create an application at https://dev.twitter.com 10 | # 11 | # HUBOT_DOCS_GITHUB_USER 12 | # HUBOT_DOCS_GITHUB_EMAIL 13 | # HUBOT_DOCS_GITHUB_TOKEN 14 | # 15 | # Examples: 16 | # hubot docs build zendframework/zend-expressive 17 | # 18 | # Author: 19 | # Matthew Weier O'Phinney 20 | 21 | DocsBuild = require '../lib/docs-build' 22 | 23 | module.exports = (robot) -> 24 | bodyParser = require 'body-parser' 25 | 26 | robot.logger.info "docs listener registered" 27 | 28 | user = process.env.HUBOT_DOCS_GITHUB_USER 29 | email = process.env.HUBOT_DOCS_GITHUB_EMAIL 30 | token = process.env.HUBOT_DOCS_GITHUB_TOKEN 31 | docs = new DocsBuild robot, user, email, token 32 | 33 | robot.respond /docs build (.*)$/i, id: "authorize", (msg) -> 34 | repo = msg.match[1] 35 | docs.build repo, msg 36 | 37 | robot.on "build-success", (data) -> docs.build(data.repo, false) 38 | 39 | # In order to calculate signatures, we need to shove the JSON body 40 | # parser to the top of the stack and have it set the raw request body 41 | # contents in the request when done parsing. 42 | robot.router.stack.unshift { 43 | route: "/docs" 44 | handle: bodyParser.json { 45 | verify: (req, res, buf, encoding) -> 46 | req.rawBody = buf 47 | } 48 | } 49 | 50 | robot.router.post '/docs', (req, res) -> 51 | # First we check for 52 | if not req.headers? or not req.headers.hasOwnProperty "authorization" 53 | res.send 401, "Unauthorized" 54 | return 55 | 56 | header = req.headers['authorization'] 57 | if not header.match /^bearer [a-f0-9]+/i 58 | res.send 400, "Client Error: invalid authentication type" 59 | return 60 | 61 | receivedToken = header.substring 7 62 | if not receivedToken or receivedToken != process.env.HUBOT_DOCS_API_TOKEN 63 | res.send 403, "Forbidden" 64 | return 65 | 66 | data = req.body 67 | 68 | if not data.repo? or not data.repo.match /zendframework\/[a-z0-9]+/ 69 | res.send 422, "Missing or malformed required repo property" 70 | return 71 | 72 | # We can now accept it; return a response immediately, and then process 73 | res.send 202 74 | 75 | msg = { 76 | send: (msg) -> 77 | robot.logger.info msg 78 | } 79 | 80 | docs.build data.repo, msg 81 | -------------------------------------------------------------------------------- /scripts/error-handler.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Default error handler. 3 | 4 | ErrorHandler = require "../lib/error-handler" 5 | 6 | module.exports = (robot) -> 7 | HUBOT_ERROR_ROOM = if process.env.HUBOT_ERROR_ROOM? then process.env.HUBOT_ERROR_ROOM else false 8 | 9 | handler = new ErrorHandler robot, HUBOT_ERROR_ROOM 10 | robot.error (err, res) -> handler.listen err, res 11 | -------------------------------------------------------------------------------- /scripts/github.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Subscribe repositories to GitHub webhook events, and provide general GitHub 3 | # integration. For example, any mention of "org/repo#1" will cause the bot to 4 | # emit a link to the referenced issue or pull request. 5 | # 6 | # Commands: 7 | # hubot github follow - Start following the specified repository in this channel; should be in the form / 8 | # hubot github unfollow - Stop following the specified repository in this channel 9 | # hubot github list - List repositories followed in this channel 10 | # hubot github clear - Stop following all repositories in this channel 11 | # 12 | # Configuration: 13 | # 14 | # The following environment variables are required. 15 | # 16 | # HUBOT_GITHUB_TOKEN: Authentication token to use with GitHub API 17 | # HUBOT_GITHUB_CALLBACK_URL_BASE: Base URL for event callbacks. 18 | # HUBOT_GITHUB_CALLBACK_SECRET: Secret to use for new subscriptions; used to verify event payloads. 19 | # HUBOT_GITHUB_DEFAULT_ORG: Default user/org to use when not provided with a repository specification 20 | # 21 | # Examples: 22 | # hubot github follow zendframework/zend-stdlib 23 | # hubot github unfollow zendframework/zend-expressive 24 | # hubot github list 25 | # 26 | # Author: 27 | # Matthew Weier O'Phinney 28 | 29 | module.exports = (robot) -> 30 | bodyParser = require 'body-parser' 31 | github_push = require '../lib/github-push' 32 | github_issues = require '../lib/github-issues' 33 | github_issue_comment = require '../lib/github-issue-comment' 34 | github_pull_request = require '../lib/github-pull-request' 35 | github_pull_request_review = require '../lib/github-pull-request-review' 36 | github_pull_request_review_comment = require '../lib/github-pull-request-review-comment' 37 | github_release = require '../lib/github-release' 38 | github_status = require '../lib/github-status' 39 | 40 | HUBOT_GITHUB_TOKEN = process.env.HUBOT_GITHUB_TOKEN 41 | HUBOT_GITHUB_DEFAULT_ORG = if process.env.HUBOT_GITHUB_DEFAULT_ORG? then process.env.HUBOT_GITHUB_DEFAULT_ORG else "zendframework" 42 | HUBOT_GITHUB_CALLBACK_URL_BASE = process.env.HUBOT_GITHUB_CALLBACK_URL_BASE 43 | HUBOT_GITHUB_CALLBACK_SECRET = process.env.HUBOT_GITHUB_CALLBACK_SECRET 44 | HUBOT_GITHUB_RELEASE_CALLBACK = process.env.HUBOT_GITHUB_RELEASE_CALLBACK 45 | 46 | githubSub = new github_push robot, HUBOT_GITHUB_CALLBACK_URL_BASE, HUBOT_GITHUB_TOKEN, HUBOT_GITHUB_CALLBACK_SECRET 47 | 48 | robot.respond /github follow (.*)$/i, id: "authorize", (msg) -> 49 | repo = msg.match[1] 50 | if not repo.match(/^[^/]+\/[^/]+$/) 51 | repo = "#{HUBOT_GITHUB_DEFAULT_ORG}/#{repo}" 52 | githubSub.subscribe msg, repo 53 | 54 | robot.respond /github unfollow (.*)$/i, id: "authorize", (msg) -> 55 | repo = msg.match[1] 56 | if not repo.match(/^[^/]+\/[^/]+$/) 57 | repo = "#{HUBOT_GITHUB_DEFAULT_ORG}/#{repo}" 58 | githubSub.unsubscribe msg, repo 59 | 60 | robot.respond /github list/i, (msg) -> 61 | githubSub.list msg 62 | 63 | robot.respond /github clear/i, id: "authorize", (msg) -> 64 | githubSub.clear msg 65 | 66 | robot.hear /([a-z0-9][a-z0-9_.-]+\/[a-z0-9][a-z0-9_.-]+)\#(\d+)/, (msg) -> 67 | msg.send "Mentioned " 68 | 69 | # In order to calculate signatures, we need to shove the JSON body 70 | # parser to the top of the stack and have it set the raw request body 71 | # contents in the request when done parsing. 72 | robot.router.stack.unshift { 73 | route: "/github" 74 | handle: bodyParser.json { 75 | verify: (req, res, buf, encoding) -> 76 | req.rawBody = buf 77 | } 78 | } 79 | 80 | robot.router.post '/github/:room/:event', (req, res) -> 81 | room = req.params.room 82 | event = req.params.event 83 | 84 | if event not in githubSub.events 85 | res.send 203, "Unrecognized event #{event}" 86 | robot.logger.error "Unrecognized github event '#{event}' was pinged" 87 | return 88 | 89 | if not githubSub.verifySignature(req) 90 | res.send 203, "Invalid or missing signature" 91 | robot.logger.error "Invalid payload submitted to /github/#{room}/#{event}; signature invalid" 92 | return 93 | 94 | data = req.body 95 | 96 | if not githubSub.verifyRoom(data.repository.full_name, room) 97 | res.send 203, "Unrecognized" 98 | robot.logger.error "Invalid payload submitted to /github/#{room}/#{req.params.event}; no repo '#{data.repository.full_name}' hooks registered for this room" 99 | return 100 | 101 | # We can now accept it; return a response immediately, and then process 102 | res.send 202 103 | 104 | # Now, we need to switch on the event, and determine what message to send 105 | # to the room. 106 | switch event 107 | when "issues" 108 | github_issues robot, room, data 109 | when "issue_comment" 110 | github_issue_comment robot, room, data 111 | when "pull_request" 112 | github_pull_request robot, room, data 113 | when "pull_request_review" 114 | github_pull_request_review robot, room, data 115 | when "pull_request_review_comment" 116 | github_pull_request_review_comment robot, room, data 117 | when "release" 118 | github_release robot, room, data, HUBOT_GITHUB_RELEASE_CALLBACK, HUBOT_GITHUB_CALLBACK_SECRET 119 | when "status" 120 | github_status robot, room, data, HUBOT_GITHUB_TOKEN 121 | 122 | -------------------------------------------------------------------------------- /scripts/tweetstream.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Subscribe to tweets matching keywords 3 | # 4 | # Commands: 5 | # hubot twitter track - Start watching a keyword 6 | # hubot twitter untrack - Stop watching a keyword 7 | # hubot twitter follow - Start following tweets from @screen_name 8 | # hubot twitter unfollow - Stop following tweets from @screen_name 9 | # hubot twitter list - Get the watched keywords and users list in current room 10 | # hubot twitter clear - Stop watching all keywords and users in current room 11 | # hubot tweet - Tweet a message from the configured twitter account 12 | # hubot retweet - Retweet a message from the configured twitter account. Provide either a tweet ID, or a tweet URI. 13 | # 14 | # Configuration: 15 | # 16 | # The following environment variables are required. You will need to create an application at https://dev.twitter.com 17 | # 18 | # HUBOT_TWITTER_CONSUMER_KEY 19 | # HUBOT_TWITTER_CONSUMER_SECRET 20 | # HUBOT_TWITTER_ACCESS_TOKEN_KEY 21 | # HUBOT_TWITTER_ACCESS_TOKEN_SECRET 22 | # 23 | # The following environment variables are optional: 24 | # 25 | # HUBOT_TWITTER_CLEAN_SUBSCRIPTIONS: Clear all subscriptions at boot time. 26 | # 27 | # Examples: 28 | # hubot twitter track github 29 | # hubot twitter follow nodejs 30 | # 31 | # Author: 32 | # Matthew Weier O'Phinney 33 | 34 | Tweeter = require '../lib/twitter-tweeter' 35 | TweetStream = require '../lib/twitter-tweetstream' 36 | Twit = require('twit') 37 | 38 | module.exports = (robot) -> 39 | 40 | AUTH = 41 | consumer_key: process.env.HUBOT_TWITTER_CONSUMER_KEY 42 | consumer_secret: process.env.HUBOT_TWITTER_CONSUMER_SECRET 43 | access_token: process.env.HUBOT_TWITTER_ACCESS_TOKEN_KEY 44 | access_token_secret: process.env.HUBOT_TWITTER_ACCESS_TOKEN_SECRET 45 | 46 | twit = new Twit(AUTH) 47 | tweeter = new Tweeter(robot, twit) 48 | tweetStream = new TweetStream(robot, twit, process.env.HUBOT_TWITTER_CLEAN_SUBSCRIPTIONS?) 49 | 50 | robot.respond /twitter clear/i, id: "authorize", (msg) -> tweetStream.clear(msg) 51 | robot.respond /twitter follow (.*)$/i, id: "authorize", (msg) -> tweetStream.follow(msg) 52 | robot.respond /twitter list/i, (msg) -> tweetStream.list(msg) 53 | robot.respond /twitter unfollow (.*)$/i, id: "authorize", (msg) -> tweetStream.unfollow(msg) 54 | robot.respond /twitter untrack (.*)$/i, id: "authorize", (msg) -> tweetStream.untrack(msg) 55 | robot.respond /twitter track (.*)$/i, id: "authorize", (msg) -> tweetStream.track(msg) 56 | 57 | robot.respond /tweet (.*)$/i, id: "authorize", (msg) -> 58 | text = msg.match[1] 59 | if text.length > 280 60 | msg.send "That tweet message is too long (#{text.length} characters); please shorten it to 280 characters." 61 | return 62 | tweeter.tweet { status: text }, (data) => 63 | msg.send "Tweet sent! https://twitter.com/#{data.screen_name}/status/#{data.id_str}" 64 | 65 | robot.respond /retweet (.*)$/i, id: "authorize", (msg) -> 66 | tweet_id = msg.match[1] 67 | tweeter.retweet tweet_id, (data) => 68 | msg.send "Message retweeted! https://twitter.com/#{data.screen_name}/status/#{data.id_str}" 69 | 70 | robot.brain.on "loaded", (data) -> tweetStream.load(data) 71 | 72 | robot.on "tweet", (tweet_data) -> tweeter.tweet(tweet_data) 73 | -------------------------------------------------------------------------------- /scripts/zf-acls.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Zend Framework SlackBot ACLs: add and remove users from ACL whitelist, 3 | # allowing them to perform other bot actions. Listen to events with the 4 | # listener metadata `id: "authorize"`, and test the user against the stored 5 | # ACLs to determine if the action may continue. 6 | # 7 | # Commands: 8 | # hubot acl allow - Add a user to the whitelist. 9 | # hubot acl deny - Remove a user from the whitelist. 10 | # hubot acl list - List users in the whitelist. 11 | # 12 | # Configuration: 13 | # 14 | # The following environment variables are optional: 15 | # 16 | # HUBOT_ZF_ACL_USER_WHITELIST: Comma-separated list of users allowed by default. 17 | # HUBOT_ZF_ACL_CLEAR_WHITELIST: If present, clears the existing whitelist prior to loading the ACLs. 18 | # HUBOT_VERBOSE: Flag indicating whether or not to be verbose in output. 19 | # 20 | # Examples: 21 | # 22 | # hubot acl allow akrabat 23 | # hubot acl deny ocramius 24 | # hubot acl list 25 | # 26 | # Author: 27 | # Matthew Weier O'Phinney 28 | 29 | ZfAcl = require "../lib/zf-acl" 30 | 31 | module.exports = (robot) -> 32 | 33 | user_whitelist = [] 34 | if process.env.HUBOT_ZF_ACL_USER_WHITELIST 35 | user_whitelist = process.env.HUBOT_ZF_ACL_USER_WHITELIST 36 | user_whitelist = user_whitelist.split ',' 37 | 38 | acl = new ZfAcl robot, user_whitelist, process.env.HUBOT_VERBOSE? 39 | 40 | robot.respond /acl list/i, id: "authorize", (msg) -> acl.list(msg) 41 | robot.respond /acl allow (.*)$/i, id: "authorize", (msg) -> acl.allow(msg) 42 | robot.respond /acl deny (.*)$/i, id: "authorize", (msg) -> acl.deny(msg) 43 | robot.brain.on "loaded", (data) -> acl.load(data) 44 | 45 | # Register a listener to handle authorization. 46 | robot.listenerMiddleware (context, next, done) -> 47 | return next() if not context.listener.options?.id? 48 | return next() if not context.listener.options.id == "authorize" 49 | 50 | if not acl.verify(context.response) 51 | context.response.send "You are not authorized to do that." 52 | done() 53 | return 54 | 55 | next() 56 | -------------------------------------------------------------------------------- /var/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 30 | 31 |
32 |

ZFBot

33 |

Chatbot for the Zend Framework community

34 |
35 | 36 | 37 | --------------------------------------------------------------------------------