├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE.md ├── README.md ├── app.js ├── app.json ├── aws ├── api-swagger-template.json ├── brick │ ├── .gitignore │ ├── brick.yaml │ ├── build.sh │ ├── index.ic │ └── params.icp ├── config.example.sh ├── deploy.sh ├── sam-template.json └── src │ └── lambda.js ├── azuredeploy.json ├── bin └── www ├── config.js ├── iisnode.yaml ├── lib └── badge.js ├── locales ├── cs.json ├── de.json ├── en.json ├── es.json ├── fr.json ├── it.json ├── ja.json ├── ko.json ├── nl.json ├── pl.json ├── pt-BR.json ├── pt.json ├── ru.json ├── tr.json ├── zh-CN.json └── zh-TW.json ├── manifest.yml ├── package-lock.json ├── package.json ├── public ├── css │ └── style.css └── images │ ├── bg.jpg │ └── field.png ├── routes └── index.js ├── screenshots ├── badge.png ├── basic_info-client_id.png ├── join-page.jpg ├── legacy-token.gif ├── oauth1.gif ├── oauth2.gif ├── oauth3.gif ├── oauth4.gif └── recaptcha.gif └── views ├── error.pug ├── index.pug └── result.pug /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | 30 | .env 31 | .env.* 32 | .idea/ 33 | .envrc 34 | 35 | aws/config.sh 36 | aws/build/ 37 | aws/api-swagger.json 38 | aws/sam-output.yml 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | node_js: 4 | - '8' 5 | 6 | install: 7 | - npm install 8 | 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | 3 | EXPOSE 3000 4 | 5 | COPY . /slack-invite-automation 6 | WORKDIR /slack-invite-automation 7 | 8 | RUN npm install 9 | 10 | CMD node ./bin/www 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 "Outsider" Jeonghoon Byun 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Slack Invite Automation 2 | ------------ 3 | 4 | [![Build Status](https://travis-ci.com/outsideris/slack-invite-automation.svg?branch=master)](https://travis-ci.com/outsideris/slack-invite-automation) 5 | 6 | A tiny web application to invite a user into your Slack team. 7 | 8 | Inspired by 9 | [How I hacked Slack into a community platform with Typeform](https://levels.io/slack-typeform-auto-invite-sign-ups/) 10 | and Socket.io's Slack page. 11 | 12 | This project supports Heroku, Azure, Cloud Foundry, Amazon Web Services (AWS), and [ic.dev](https://ic.dev). 13 | 14 | [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy) 15 | [![Deploy to Azure](https://azuredeploy.net/deploybutton.png)](https://azuredeploy.net/) 16 | 17 | ## Settings 18 | 19 | You can set variables for your own purpose in `config.js` or environment variables. 20 | 21 | ### `config.js` 22 | 23 | Fill out `config.js` as your infomation. 24 | 25 | * `community`: your community or team name to display on join page. 26 | * `slackUrl` : your slack team url (ex.: socketio.slack.com) 27 | * `slacktoken` : Your access token for Slack. (see [Issue token](#issue-token)) 28 | * `inviteToken`: An optional security measure - if it is set, then that token will be required to get invited. 29 | * `recaptchaSiteKey`: An optional security measure - if it is set, and `recaptchaSecretKey` is set, then a captcha will be required to get invited. 30 | * `recaptchaSecretKey`: An optional security measure - if it is set, and `recaptchaSiteKey` is set, then a captcha will be required to get invited. 31 | * `locale`: Application language (currently `cs`, `de`, `en`, `es`, `fr`, `it`, `ja`, `ko`, `nl`, `pl`, `pt`, `pt-BR`, `tr`, `zh-CN` and `zh-TW` available). 32 | * `subpath`: Sub-path in URL. For example, if `/example` is set, it's served in `/example`, not `/`. Default is `/`. 33 | 34 | ### Environment Variables 35 | You can set environment variables directly or in `.env` file. 36 | If you want to use a `.env` file, create a file in the root called `.env` with the following key/value pairs. 37 | (`.env` files are added to the `.gitignore`.) 38 | 39 | - `COMMUNITY_NAME` : Your community or team name to display on join page. 40 | - `SLACK_URL` : Your Slack team url (ex.: socketio.slack.com) 41 | - `SLACK_TOKEN` : Your access token for Slack. (see [Issue token](#issue-token)) 42 | - `INVITE_TOKEN`: An optional security measure - if it is set, then that token will be required to get invited. 43 | - `RECAPTCHA_SITE`: An optional security measure - used to enable reCAPTCHA. 44 | - `RECAPTCHA_SECRET`: An optional security measure - used to enable reCAPTCHA. 45 | - `LOCALE`: Application language (currently `cs`, `de`, `en`, `es`, `fr`, `it`, `ja`, `ko`, `nl`, `pl`, `pt`, `pt-BR`, `tr`, `zh-CN` and `zh-TW` available). 46 | - `SUBPATH`: Sub-path in URL. For example, if `/example` is set, it's served in `/example`, not `/`. Default is `/`. 47 | 48 | **Sample** 49 | 50 | ``` 51 | COMMUNITY_NAME=socketio 52 | SLACK_URL=socketio.slack.com 53 | SLACK_TOKEN=ffsdf-5411524512154-16875416847864648976-45641654654654654-444334f43b34566f 54 | INVITE_TOKEN=abcdefg 55 | LOCALE=en 56 | ``` 57 | 58 | You can test your token via curl: 59 | 60 | ```shell 61 | curl -X POST 'https://YOUR-SLACK-TEAM.slack.com/api/users.admin.invite' \ 62 | --data 'email=EMAIL&token=TOKEN&set_active=true' \ 63 | --compressed 64 | ``` 65 | 66 | ### Heroku / Azure 67 | 68 | Add the application settings that are defined in the environment variables above. 69 | 70 | ### Amazon Web Services (AWS) 71 | 72 | If you have an AWS account and have already installed and configured the AWS CLI tool, you can easily deploy this application to API Gateway and Lambda via CloudFormation in a few minutes. 73 | 74 | Instead of editing `config.js`, take these steps: 75 | 76 | 1. Copy `aws/config.example.sh` to `aws/config.sh` 77 | 2. Edit the values in `aws/config.sh`, which correspond to the variables described above, plus these: 78 | * `StackName`: the name of the CloudFormation stack to create 79 | * `S3BucketArtifacts`: the name of an existing S3 bucket you have write access to, for storing deployment artifacts 80 | * `S3PrefixArtifacts`: the prefix to use within that S3 bucket for all deployment artifacts written 81 | 3. Run `aws/deploy.sh` to create the CloudFormation stack and deploy your application, outputting the URL 82 | 4. (Optional) For a friendlier URL, log into the AWS web console and establish a custom domain pointing to the API Gateway stage deployed in step 3. 83 | 84 | ### [ic.dev](https://ic.dev) 85 | 86 | If you haven't already installed the IC CLI, please refer to the [documentation](https://ic.dev/docs/en/installation). 87 | 88 | Deploy the `lsuss.slack_inviter` brick directly from the IC Public Index: 89 | ```shell 90 | $ ic aws up lsuss.slack_inviter slack_inviter --params community_name='Your Community Name',slack_url=yourcommunity.slack.com,slack_token=xoxp-xxx-xxx-xxx-xxx 91 | ``` 92 | 93 | Retreive the id and url of the API: 94 | ```shell 95 | $ ic aws value slack_inviter 96 | ``` 97 | 98 | ## Run 99 | [Node.js](http://nodejs.org/) is required. 100 | 101 | ```shell 102 | $ git clone https://github.com/outsideris/slack-invite-automation.git 103 | $ cd slack-invite-automation 104 | $ npm install 105 | $ npm start 106 | ``` 107 | 108 | You can access on your web browser. 109 | 110 | ![](screenshots/join-page.jpg) 111 | 112 | ## Run with Docker 113 | 114 | It's easy to run this service if you have installed Docker on your system. 115 | Pull [the Docker image from Docker Hub](https://hub.docker.com/r/outsideris/slack-invite-automation/). 116 | 117 | ```shell 118 | $ docker pull outsideris/slack-invite-automation 119 | $ docker run -it --rm -e COMMUNITY_NAME="YOUR-TEAM-NAME" -e SLACK_URL="YOUR-TEAM.slack.com" -e SLACK_TOKEN="YOUR-ACCESS-TOKEN" -p 3000:3000 outsideris/slack-invite-automation 120 | ``` 121 | 122 | Or, You can build a Docker image yourself. 123 | 124 | ```shell 125 | $ git clone https://github.com/outsideris/slack-invite-automation.git 126 | $ cd slack-invite-automation 127 | $ docker build -t outsideris/slack-invite-automation . 128 | $ docker run -it --rm -e COMMUNITY_NAME="YOUR-TEAM-NAME" -e SLACK_URL="YOUR-TEAM.slack.com" -e SLACK_TOKEN="YOUR-ACCESS-TOKEN" -p 3000:3000 outsideris/slack-invite-automation 129 | ``` 130 | 131 | ## Issue token 132 | **You should generate the token in admin user, not owner.** If you generate the token in owner user, a `missing_scope` error may occur. 133 | 134 | There are two ways to issue the access token. 135 | 136 | ### Legacy tokens 137 | 1. Visit . 138 | 1. Click `Create token`. 139 | 140 | ![](screenshots/legacy-token.gif) 141 | 142 | ### OAuth tokens 143 | 1. Visit and Create New App. 144 | 145 | ![](screenshots/oauth1.gif) 146 | 147 | 1. Click "Permissions". 148 | 149 | ![](screenshots/oauth2.gif) 150 | 151 | 1. In "OAuth & Permissions" page, select `admin` scope under "Permission Scopes" menu and save changes. 152 | 153 | ![](screenshots/oauth3.gif) 154 | 155 | 1. Click "Install App to Workspace". 156 | 157 | ![](screenshots/oauth4.gif) 158 | 159 | 1. Visit in your browser and authorize your app. 160 | * This form requires the `client` permission. Otherwise, you can see `{"ok":false,"error":"missing_scope","needed":"client","provided":"admin"}` error. 161 | * Your `TEAM_ID` is the subdomain for your slack team, e.g. myteam.slack.com - your TEAM_ID is `myteam`. 162 | * Your `CLIENT_ID` found in "Basic Information" section for your App. 163 | * You will be shown a `Installed App Settings > OAuth Tokens for Your Team` screen. 164 | * You can test auto invites with curl by providing the `OAuth Access Token`. 165 | ```sh 166 | curl -X POST 'https://myteam.slack.com/api/users.admin.invite' \ 167 | --data 'email=test@email.com&token=OAuthAccessToken&set_active=true' \ 168 | --compressed 169 | ``` 170 | 171 | ![](screenshots/basic_info-client_id.png) 172 | 173 | ## Badge 174 | 175 | ![](screenshots/badge.png) 176 | 177 | You can use the badge to show status of user in your slack. 178 | 179 | * With default colors: 180 | ``` 181 | 182 | ``` 183 | 184 | * With custom colors: 185 | 186 | * `?colorA=abcdef` Set background of the left part (hex color only) 187 | * `?colorB=fedcba` Set background of the right part (hex color only) 188 | 189 | ``` 190 | 191 | ``` 192 | 193 | ## reCAPTCHA 194 | Register a new site in [Google reCAPTHCA](https://www.google.com/recaptcha/) 195 | as reCAPTCHA v2 type. 196 | 197 | ![](screenshots/recaptcha.gif) 198 | 199 | Set "Site key" as `recaptchaSiteKey` or `RECAPTCHA_SITE`, 200 | and "Secret key" as `recaptchaSecretKey` or `RECAPTCHA_SECRET`. 201 | 202 | ## Associate fork with heroku 203 | If you use the "Deploy to Heroku" button and want to modify your App you should 204 | fork this project. After forking and making changes you should associate your 205 | repo with the deployed instance by running: 206 | 207 | `$ heroku git:remote -a thawing-inlet-61413` replacing your heroku app's name 208 | and running 209 | 210 | `$ git push heroku master` to upload the changes. For full details see 211 | [Heroku: deploying with git](https://devcenter.heroku.com/articles/git#for-an-existing-heroku-app) 212 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const favicon = require('serve-favicon'); 4 | const logger = require('morgan'); 5 | const cookieParser = require('cookie-parser'); 6 | const bodyParser = require('body-parser'); 7 | const i18n = require("i18n"); 8 | 9 | const config = require('./config'); 10 | 11 | const routes = require('./routes/index'); 12 | 13 | const app = express(); 14 | 15 | i18n.configure({ 16 | defaultLocale: "en", 17 | directory: __dirname + '/locales', 18 | autoReload: true 19 | }); 20 | 21 | i18n.setLocale(config.locale); 22 | 23 | // default: using 'accept-language' header to guess language settings 24 | app.use(i18n.init); 25 | 26 | // view engine setup 27 | app.set('views', path.join(__dirname, 'views')); 28 | app.set('view engine', 'pug'); 29 | 30 | // uncomment after placing your favicon in /public 31 | //app.use(favicon(__dirname + '/public/favicon.ico')); 32 | app.use(logger('dev')); 33 | app.use(bodyParser.json()); 34 | app.use(bodyParser.urlencoded({ extended: false })); 35 | app.use(cookieParser()); 36 | app.use(express.static(path.join(__dirname, 'public'))); 37 | 38 | app.use(config.subpath, routes); 39 | 40 | // catch 404 and forward to error handler 41 | app.use(function(req, res, next) { 42 | const err = new Error('Not Found'); 43 | err.status = 404; 44 | next(err); 45 | }); 46 | 47 | // error handlers 48 | 49 | // development error handler 50 | // will print stacktrace 51 | if (app.get('env') === 'development') { 52 | app.use(function(err, req, res, next) { 53 | res.status(err.status || 500); 54 | res.render('error', { 55 | message: err.message, 56 | error: err 57 | }); 58 | }); 59 | } 60 | 61 | // production error handler 62 | // no stacktraces leaked to user 63 | app.use(function(err, req, res, next) { 64 | res.status(err.status || 500); 65 | res.render('error', { 66 | message: err.message, 67 | error: {} 68 | }); 69 | }); 70 | 71 | 72 | module.exports = app; 73 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Slack Invite Automation", 3 | "description": "A tiny web application to invite a user into your slack team.", 4 | "repository": "https://github.com/outsideris/slack-invite-automation", 5 | "keywords": ["node", "slack", "invite"], 6 | "env": { 7 | "COMMUNITY_NAME": { 8 | "description": "Your team name", 9 | "value": "YOUR TEAM NAME" 10 | }, 11 | "SLACK_URL": { 12 | "description": "Your slack url(ex: socketio.slack.com)", 13 | "value": "YOUR-TEAM.slack.com" 14 | }, 15 | "SLACK_TOKEN": { 16 | "description": "access token of slack, You can generate it in https://api.slack.com/web#auth", 17 | "value": "" 18 | }, 19 | "INVITE_TOKEN": { 20 | "description": "Shared invite token used to get access. Leave blank if you don't want users to have to provide a token.", 21 | "value": "", 22 | "required": false 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /aws/api-swagger-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "%ApiTitle%" 6 | }, 7 | "basePath": "/", 8 | "schemes": ["https"], 9 | "consumes": ["application/json"], 10 | "produces": ["application/json"], 11 | "paths": { 12 | "/": { 13 | "x-amazon-apigateway-any-method": { 14 | "x-amazon-apigateway-integration": { 15 | "uri": "arn:aws:apigateway:%Region%:lambda:path/2015-03-31/functions/arn:aws:lambda:%Region%:%AccountId%:function:${stageVariables.ExpressFunction}/invocations", 16 | "passthroughBehavior": "when_no_match", 17 | "httpMethod": "POST", 18 | "type": "aws_proxy" 19 | } 20 | } 21 | }, 22 | "/{proxy+}": { 23 | "x-amazon-apigateway-any-method": { 24 | "parameters": [ 25 | { 26 | "name": "proxy", 27 | "in": "path", 28 | "required": true, 29 | "type": "string" 30 | } 31 | ], 32 | "x-amazon-apigateway-integration": { 33 | "uri": "arn:aws:apigateway:%Region%:lambda:path/2015-03-31/functions/arn:aws:lambda:%Region%:%AccountId%:function:${stageVariables.ExpressFunction}/invocations", 34 | "httpMethod": "POST", 35 | "type": "aws_proxy" 36 | } 37 | } 38 | } 39 | }, 40 | "x-amazon-apigateway-binary-media-types": ["*/*"] 41 | } 42 | -------------------------------------------------------------------------------- /aws/brick/.gitignore: -------------------------------------------------------------------------------- 1 | lambda.zip 2 | build/ -------------------------------------------------------------------------------- /aws/brick/brick.yaml: -------------------------------------------------------------------------------- 1 | name: lsuss.slack_inviter 2 | version: v0.1.2 3 | license: MIT 4 | description: | 5 | A serverless web application to invite users into your Slack 6 | main: :brick 7 | require: 8 | iclab.simple: v0.2.0 9 | assets: 10 | - lambda.zip 11 | -------------------------------------------------------------------------------- /aws/brick/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # "set -e" makes it so if any step fails, the script aborts: 4 | set -e 5 | 6 | cd "${BASH_SOURCE%/*}" 7 | 8 | # Build Lambda package 9 | rm -rf build 10 | mkdir build 11 | cd ../.. 12 | cp -r app.js config.js lib locales package-lock.json package.json public routes views aws/brick/build 13 | cd aws/brick/build 14 | cp ../../src/* . 15 | npm install 16 | npm install aws-serverless-express 17 | cd ../build 18 | zip -r ../lambda.zip . -------------------------------------------------------------------------------- /aws/brick/index.ic: -------------------------------------------------------------------------------- 1 | from iclab import simple 2 | from . import assets 3 | 4 | 5 | @resource 6 | def brick( 7 | community_name, 8 | slack_url, 9 | slack_token, 10 | invite_token="", 11 | recaptcha_site_key="", 12 | recaptcha_secret_key="", 13 | locale="en", 14 | ): 15 | api = simple.api("api", "express", "0.1.2") 16 | func = simple.function( 17 | "func", 18 | "nodejs8.10", 19 | assets["lambda.zip"], 20 | handler="lambda.handler", 21 | memory_size=1024, 22 | timeout=10, 23 | environ=dict( 24 | COMMUNITY_NAME=community_name, 25 | SLACK_URL=slack_url, 26 | SLACK_TOKEN=slack_token, 27 | INVITE_TOKEN=invite_token, 28 | RECAPTCHA_SITE=recaptcha_site_key, 29 | RECAPTCHA_SECRET=recaptcha_secret_key, 30 | LOCALE=locale, 31 | ), 32 | ) 33 | func.http(api, "any", "/{proxy+}", binary_media="*/*") 34 | func.http(api, "any", "/", binary_media="*/*") 35 | 36 | return dict(api=dict(id=api["api"]["ref"], url=f'{api["url"]}/')) 37 | -------------------------------------------------------------------------------- /aws/brick/params.icp: -------------------------------------------------------------------------------- 1 | community_name = "Your Community Name" 2 | slack_url = "yourcommunity.slack.com" 3 | slack_token = "xoxp-xxx-xxx-xxx-xxx" 4 | # invite_token = util.sensitive("") 5 | # recaptcha_site_key = util.sensitive("") 6 | # recaptcha_secret_key = util.sensitive("") 7 | # locale = "en" 8 | -------------------------------------------------------------------------------- /aws/config.example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | StackName=SlackInviter 4 | 5 | S3BucketArtifacts=your-artifact-bucket 6 | S3PrefixArtifacts=cloudformation/slack-invite-automation 7 | 8 | CommunityName="Your Community Name" 9 | SlackUrl=yourcommunity.slack.com 10 | SlackToken=xoxp-xxx-xxx-xxx-xxx 11 | InviteToken= 12 | RecaptchaSiteKey= 13 | RecaptchaSecretKey= 14 | Locale=en 15 | 16 | -------------------------------------------------------------------------------- /aws/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # "set -e" makes it so if any step fails, the script aborts: 4 | set -e 5 | 6 | cd "${BASH_SOURCE%/*}" 7 | source ./config.sh 8 | 9 | AwsRegion=$(aws configure get region) 10 | AwsAccountId=$(aws sts get-caller-identity --output text --query Account) 11 | 12 | # Build Lambda package 13 | rm -rf build 14 | mkdir build 15 | cd .. 16 | cp -r app.js config.js lib locales package-lock.json package.json public routes views aws/build 17 | cd aws/build 18 | cp ../src/* . 19 | npm install 20 | npm install aws-serverless-express 21 | cd .. 22 | 23 | # Replace variables (Region and AccountId) in API Swagger template 24 | cat api-swagger-template.json | sed -e "s/%Region%/${AwsRegion}/g" | sed -e "s/%AccountId%/${AwsAccountId}/g" | sed -e "s/%ApiTitle%/${StackName}/g"> api-swagger.json 25 | 26 | # Package SAM template (loads Lambda dist zips to S3 locations) 27 | aws cloudformation package \ 28 | --template-file sam-template.json \ 29 | --output-template-file sam-output.yml \ 30 | --s3-bucket "${S3BucketArtifacts}" \ 31 | --s3-prefix "${S3PrefixArtifacts}" 32 | 33 | # Deploy CloudFormation stack 34 | aws cloudformation deploy \ 35 | --template-file sam-output.yml \ 36 | --stack-name "${StackName}" \ 37 | --capabilities CAPABILITY_IAM \ 38 | --parameter-overrides \ 39 | CommunityName="${CommunityName}" \ 40 | SlackUrl="${SlackUrl}" \ 41 | SlackToken="${SlackToken}" \ 42 | InviteToken="${InviteToken}" \ 43 | RecaptchaSiteKey="${RecaptchaSiteKey}" \ 44 | RecaptchaSecretKey="${RecaptchaSecretKey}" \ 45 | Locale="${Locale}" 46 | 47 | Url=$(aws cloudformation describe-stacks --stack-name ${StackName} | grep OutputValue | cut -f 4 -d'"') 48 | 49 | echo 50 | echo 'Deployed Slack Inviter!' 51 | echo 52 | echo "Account Id: ${AwsAccountId}" 53 | echo " Region: ${AwsRegion}" 54 | echo "Stack Name: ${StackName}" 55 | echo " URL: ${Url}" 56 | -------------------------------------------------------------------------------- /aws/sam-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Transform": "AWS::Serverless-2016-10-31", 4 | "Description": "Slack Inviter", 5 | "Parameters": { 6 | "CommunityName": { 7 | "Type": "String", 8 | "Description": "Name of your Slack community (e.g., \"Example Team\")" 9 | }, 10 | "SlackUrl": { 11 | "Type": "String", 12 | "Description": "URL host for your Slack community (e.g., \"exampleteam.slack.com\")" 13 | }, 14 | "SlackToken": { 15 | "Type": "String", 16 | "NoEcho": true, 17 | "Description": "Slack access token" 18 | }, 19 | "InviteToken": { 20 | "Type": "String", 21 | "Description": "Optional invite token" 22 | }, 23 | "RecaptchaSiteKey": { 24 | "Type": "String", 25 | "Description": "Optional Recaptcha site key" 26 | }, 27 | "RecaptchaSecretKey": { 28 | "Type": "String", 29 | "NoEcho": true, 30 | "Description": "Optional Recaptcha secret key" 31 | }, 32 | "Locale": { 33 | "Type": "String", 34 | "Description": "Optional locale string (e.g., \"it\")" 35 | } 36 | }, 37 | "Resources": { 38 | "SlackInviterApi": { 39 | "Type": "AWS::Serverless::Api", 40 | "Properties": { 41 | "DefinitionUri": "api-swagger.json", 42 | "StageName": "Express", 43 | "Variables": { 44 | "ExpressFunction": { "Ref": "ExpressFunction" } 45 | } 46 | } 47 | }, 48 | "ExpressFunction": { 49 | "Type": "AWS::Serverless::Function", 50 | "Properties": { 51 | "FunctionName": { "Fn::Sub": "${AWS::StackName}-Express" }, 52 | "Description": "Express API handler", 53 | "CodeUri": "build/", 54 | "Handler": "lambda.handler", 55 | "Runtime": "nodejs8.10", 56 | "MemorySize": 1024, 57 | "Timeout": 10, 58 | "Tracing": "Active", 59 | "Policies": ["AWSXrayWriteOnlyAccess"], 60 | "Environment": { 61 | "Variables": { 62 | "COMMUNITY_NAME": { "Ref": "CommunityName" }, 63 | "SLACK_URL": { "Ref": "SlackUrl" }, 64 | "SLACK_TOKEN": { "Ref": "SlackToken" }, 65 | "INVITE_TOKEN": { "Ref": "InviteToken" }, 66 | "RECAPTCHA_SITE": { "Ref": "RecaptchaSiteKey" }, 67 | "RECAPTCHA_SECRET": { "Ref": "RecaptchaSecretKey" }, 68 | "LOCALE": { "Ref": "Locale" } 69 | } 70 | }, 71 | "Events": { 72 | "ExpressProxyApiRoot": { 73 | "Type": "Api", 74 | "Properties": { 75 | "RestApiId": { "Ref": "SlackInviterApi" }, 76 | "Path": "/", 77 | "Method": "ANY" 78 | } 79 | }, 80 | "ExpressProxyApiGreedy": { 81 | "Type": "Api", 82 | "Properties": { 83 | "RestApiId": { "Ref": "SlackInviterApi" }, 84 | "Path": "/{proxy+}", 85 | "Method": "ANY" 86 | } 87 | } 88 | } 89 | } 90 | } 91 | }, 92 | "Outputs": { 93 | "Url": { 94 | "Value": { 95 | "Fn::Sub": "https://${SlackInviterApi}.execute-api.${AWS::Region}.amazonaws.com/${SlackInviterApi.Stage}/" 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /aws/src/lambda.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const awsServerlessExpress = require('aws-serverless-express'); 4 | const app = require('./app'); 5 | 6 | // NOTE: If you get ERR_CONTENT_DECODING_FAILED in your browser, this is likely 7 | // due to a compressed response (e.g. gzip) which has not been handled correctly 8 | // by aws-serverless-express and/or API Gateway. Add the necessary MIME types to 9 | // binaryMimeTypes below, then redeploy (`npm run package-deploy`) 10 | const binaryMimeTypes = [ 11 | 'application/javascript', 12 | 'application/json', 13 | 'application/octet-stream', 14 | 'application/xml', 15 | 'font/eot', 16 | 'font/opentype', 17 | 'font/otf', 18 | 'image/jpeg', 19 | 'image/png', 20 | 'image/svg+xml', 21 | 'text/comma-separated-values', 22 | 'text/css', 23 | 'text/html', 24 | 'text/javascript', 25 | 'text/plain', 26 | 'text/text', 27 | 'text/xml', 28 | ]; 29 | 30 | const server = awsServerlessExpress.createServer(app, null, binaryMimeTypes); 31 | 32 | exports.handler = (event, context) => awsServerlessExpress.proxy(server, event, context); 33 | -------------------------------------------------------------------------------- /azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "siteName": { 6 | "type": "string" 7 | }, 8 | "hostingPlanName": { 9 | "type": "string" 10 | }, 11 | "siteLocation": { 12 | "type": "string", 13 | "defaultValue": "West US" 14 | }, 15 | "sku": { 16 | "type": "string", 17 | "allowedValues": [ 18 | "F1", 19 | "D1", 20 | "B1", 21 | "S1" 22 | ], 23 | "defaultValue": "F1" 24 | }, 25 | "workerSize": { 26 | "type": "string", 27 | "allowedValues": [ 28 | "0", 29 | "1", 30 | "2" 31 | ], 32 | "defaultValue": "0" 33 | }, 34 | "repoURL": { 35 | "type": "string", 36 | "defaultValue": "https://github.com/outsideris/slack-invite-automation.git" 37 | }, 38 | "branch": { 39 | "type": "string", 40 | "defaultValue": "master" 41 | }, 42 | "communityName": { 43 | "type": "string" 44 | }, 45 | "slackUrl": { 46 | "type": "string" 47 | }, 48 | "slackToken": { 49 | "type": "string" 50 | }, 51 | "inviteToken": { 52 | "type": "string" 53 | }, 54 | "recaptchaSite": { 55 | "type": "string", 56 | "defaultValue": "" 57 | }, 58 | "recaptchaSecret": { 59 | "type": "string", 60 | "defaultValue": "" 61 | }, 62 | "locale": { 63 | "type": "string", 64 | "defaultValue": "en", 65 | "allowedValues": [ 66 | "cs", 67 | "de", 68 | "en", 69 | "es", 70 | "fr", 71 | "it", 72 | "ja", 73 | "ko", 74 | "nl", 75 | "pl", 76 | "pt", 77 | "pt-BR", 78 | "tr", 79 | "zh-CN", 80 | "zh-TW" 81 | ] 82 | }, 83 | "subpath": { 84 | "type": "string", 85 | "defaultValue": "/" 86 | } 87 | }, 88 | "variables": {}, 89 | "resources": [ 90 | { 91 | "apiVersion": "2015-08-01", 92 | "name": "[parameters('hostingPlanName')]", 93 | "type": "Microsoft.Web/serverfarms", 94 | "location": "[parameters('siteLocation')]", 95 | "properties": { 96 | "name": "[parameters('hostingPlanName')]" 97 | }, 98 | "sku": { 99 | "name": "[parameters('sku')]" 100 | } 101 | }, 102 | { 103 | "apiVersion": "2015-08-01", 104 | "name": "[parameters('siteName')]", 105 | "type": "Microsoft.Web/sites", 106 | "location": "[parameters('siteLocation')]", 107 | "tags": { 108 | "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "empty" 109 | }, 110 | "dependsOn": [ 111 | "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]" 112 | ], 113 | "properties": { 114 | "name": "[parameters('siteName')]", 115 | "serverFarmId": "[parameters('hostingPlanName')]" 116 | }, 117 | "resources": [ 118 | { 119 | "apiVersion": "2015-08-01", 120 | "name": "web", 121 | "type": "config", 122 | "dependsOn": [ 123 | "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]" 124 | ], 125 | "properties": {} 126 | }, 127 | { 128 | "apiVersion": "2015-08-01", 129 | "name": "appsettings", 130 | "type": "config", 131 | "dependsOn": [ 132 | "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]" 133 | ], 134 | "properties": { 135 | "COMMUNITY_NAME": "[parameters('communityName')]", 136 | "SLACK_URL": "[parameters('slackUrl')]", 137 | "SLACK_TOKEN": "[parameters('slackToken')]", 138 | "INVITE_TOKEN": "[parameters('inviteToken')]", 139 | "RECAPTCHA_SITE": "[parameters('recaptchaSite')]", 140 | "RECAPTCHA_SECRET": "[parameters('recaptchaSecret')]", 141 | "LOCALE": "[parameters('locale')]", 142 | "SUBPATH": "[parameters('subpath')]", 143 | "WEBSITE_NODE_DEFAULT_VERSION": "8.9.4" 144 | } 145 | }, 146 | { 147 | "apiVersion": "2015-08-01", 148 | "name": "web", 149 | "type": "sourcecontrols", 150 | "dependsOn": [ 151 | "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]", 152 | "[concat('Microsoft.Web/Sites/', parameters('siteName'), '/config/web')]" 153 | ], 154 | "properties": { 155 | "RepoUrl": "[parameters('repoURL')]", 156 | "branch": "[parameters('branch')]", 157 | "IsManualIntegration": true 158 | } 159 | } 160 | ] 161 | } 162 | ], 163 | "outputs": { 164 | "siteUri": { 165 | "type": "string", 166 | "value": "[concat('https://',reference(resourceId('Microsoft.Web/sites', parameters('siteName'))).hostNames[0])]" 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // support for .env file to get loaded in to environment variables. 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | const envFile = path.join(__dirname, '../.env'); 7 | try { 8 | fs.accessSync(envFile, fs.F_OK); 9 | require('dotenv').config({path: envFile}); 10 | } catch (e) { 11 | // no env file 12 | } 13 | 14 | /** 15 | * Module dependencies. 16 | */ 17 | const app = require('../app'); 18 | const debug = require('debug')('slack-invite-automation:server'); 19 | const http = require('http'); 20 | 21 | /** 22 | * Normalize a port into a number, string, or false. 23 | * @param {int} val 24 | */ 25 | function normalizePort(val) { 26 | const port = parseInt(val, 10); 27 | if (isNaN(port)) { 28 | return val; 29 | } else if (port >= 0) { 30 | return port; 31 | } 32 | 33 | return false; 34 | } 35 | 36 | 37 | /** 38 | * Get port from environment and store in Express. 39 | */ 40 | 41 | const port = normalizePort(process.env.PORT || '3000'); 42 | app.set('port', port); 43 | 44 | /** 45 | * Create HTTP server. 46 | */ 47 | 48 | const server = http.createServer(app); 49 | 50 | /** 51 | * Listen on provided port, on all network interfaces. 52 | */ 53 | 54 | server.listen(port); 55 | server.on('error', onError); 56 | server.on('listening', onListening); 57 | 58 | /** 59 | * Event listener for HTTP server "error" event. 60 | */ 61 | 62 | function onError(error) { 63 | if (error.syscall !== 'listen') { 64 | throw error; 65 | } 66 | 67 | // handle specific listen errors with friendly messages 68 | switch (error.code) { 69 | case 'EACCES': 70 | console.error('Port ' + port + ' requires elevated privileges'); 71 | process.exit(1); 72 | break; 73 | case 'EADDRINUSE': 74 | console.error('Port ' + port + ' is already in use'); 75 | process.exit(1); 76 | break; 77 | default: 78 | throw error; 79 | } 80 | } 81 | 82 | /** 83 | * Event listener for HTTP server "listening" event. 84 | */ 85 | 86 | function onListening() { 87 | debug('Listening on port ' + server.address().port); 88 | } 89 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // your community or team name to display on join page. 3 | community: process.env.COMMUNITY_NAME || 'YOUR-TEAM-NAME', 4 | // your slack team url (ex: socketio.slack.com) 5 | slackUrl: process.env.SLACK_URL || 'YOUR-TEAM.slack.com', 6 | // access token of slack 7 | // see https://github.com/outsideris/slack-invite-automation#issue-token 8 | // 9 | // You can test your token via curl: 10 | // curl -X POST 'https://YOUR-SLACK-TEAM.slack.com/api/users.admin.invite' \ 11 | // --data 'email=EMAIL&token=TOKEN&set_active=true' \ 12 | // --compressed 13 | slacktoken: process.env.SLACK_TOKEN || 'YOUR-ACCESS-TOKEN', 14 | // an optional security measure - if it is set, then that token will be required to get invited. 15 | inviteToken: process.env.INVITE_TOKEN || null, 16 | // an optional security measure - if both are set, then recaptcha will be used. 17 | recaptchaSiteKey: process.env.RECAPTCHA_SITE || null, 18 | recaptchaSecretKey: process.env.RECAPTCHA_SECRET || null, 19 | // default locale 20 | locale: process.env.LOCALE || "en", 21 | subpath: process.env.SUBPATH || "/" 22 | }; 23 | -------------------------------------------------------------------------------- /iisnode.yaml: -------------------------------------------------------------------------------- 1 | node_env: production 2 | loggingEnabled: true 3 | logDirectory: iisnode 4 | -------------------------------------------------------------------------------- /lib/badge.js: -------------------------------------------------------------------------------- 1 | const pug = require('pug'); 2 | 3 | const title = 'slack'; 4 | 5 | function width(str) { 6 | return 6 * str.length; 7 | } 8 | 9 | const svgTmpl = 10 | `svg(xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width=wd height="20")\n` + 11 | ` linearGradient(x2="0" y2="100%")#b\n` + 12 | ` stop(offset="0" stop-color="#bbb" stop-opacity=".1")\n` + 13 | ` stop(offset="1" stop-opacity=".1")\n` + 14 | ` clipPath#a\n` + 15 | ` rect(width=wd height="20" rx="3" fill="#fff")\n` + 16 | ` g(clip-path="url(#a)")\n` + 17 | ` path(fill='#' + colorA d='M0 0h' + left + 'v20H0z')\n` + 18 | ` path(fill='#' + colorB d='M'+left + ' 0h' + (wd-left) + 'v20H' + left +'z')\n` + 19 | ` path(fill="url(#b)" d='M0 0h' + wd + 'v20H0z')\n` + 20 | ` g(fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="110")\n` + 21 | ` text(x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${title.length*54}") ${title}\n` + 22 | ` text(x="195" y="140" transform="scale(.1)" textLength="${title.length*54}") ${title}\n` + 23 | ` text(x=(wd * 5 + 195) y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength=(value.length*60)) #{value}\n` + 24 | ` text(x=(wd * 5 + 195) y="140" transform="scale(.1)" textLength=(value.length*60)) #{value}\n`; 25 | 26 | module.exports = { 27 | badge: function(presence, total, customColorA, customColorB) { 28 | const fn = pug.compile(svgTmpl); 29 | const value = `${presence}/${total}`; 30 | const left = width(title) + 8; 31 | const wd = left + width(value) + 22; 32 | const colorA = customColorA || '555'; 33 | const colorB = customColorB || '39AE85'; 34 | return fn({ value, wd, left, colorA, colorB }); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /locales/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Zadej e-mail a staň se členem skupiny %s na Slacku!", 3 | "ENTER_EMAIL": "Zadej svůj e-mail", 4 | "ENTER_TOKEN": "Zadej přihlašovací kód z pozvánky", 5 | "TITLE": "Staň se členem skupiny %s na Slacku!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Gib deine E-Mail Adresse an, um %s auf Slack beizutreten!", 3 | "ENTER_EMAIL": "Deine E-Mail Adresse", 4 | "ENTER_TOKEN": "Gib deinen Zugangscode an", 5 | "TITLE": "Tritt %s auf Slack bei!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Enter your email below to join %s on Slack!", 3 | "ENTER_EMAIL": "Enter Your Email Address", 4 | "ENTER_TOKEN": "Enter the invite token you were given", 5 | "TITLE": "Join the %s community on Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "¡Ingresa tu dirección de email debajo para unirte a %s en Slack!", 3 | "ENTER_EMAIL": "Ingresa tu dirección de email", 4 | "ENTER_TOKEN": "Ingresa el código de invitación", 5 | "TITLE": "¡Únete a la comunidad de %s en Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Entrez votre adresse mail pour rejoindre le groupe %s sur Slack!", 3 | "ENTER_EMAIL": "Entrez votre adresse mail", 4 | "ENTER_TOKEN": "Entrez votre token d'invitation", 5 | "TITLE": "Rejoignez le groupe %s sur Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Inseirisci qui sotto la tua email per unirti a %s su Slack!", 3 | "ENTER_EMAIL": "Inserisci il tuo indirizzo e-mail", 4 | "ENTER_TOKEN": "Inserisci il token d'invito che ti è stato fornito", 5 | "TITLE": "Unisciti a %s su Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Slackで %s に参加するためにメールアドレスを入力してください!", 3 | "ENTER_EMAIL": "メールアドレスを入力してください", 4 | "ENTER_TOKEN": "あなたが取得した招待用トークンを入力してください。", 5 | "TITLE": "Slackで %s コミュニティに参加しましょう!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Slack의 %s에 가입하기 위한 이메일 주소를 입력하세요!", 3 | "ENTER_EMAIL": "이메일 주소를 입력하세요", 4 | "ENTER_TOKEN": "초대 토큰을 입력하세요", 5 | "TITLE": "Slack의 %s 커뮤니티에 참여하세요!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Voer uw email adres in om deel te nemen aan %s op Slack!", 3 | "ENTER_EMAIL": "Voer uw email adres in", 4 | "ENTER_TOKEN": "Voer het token in dat u kreeg bij uw uitnodiging", 5 | "TITLE": "Neem deel aan de %s gemeenschap op Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Podaj swój adres email, aby dołączyć do %s na Slacku!", 3 | "ENTER_EMAIL": "Podaj swój adres email", 4 | "ENTER_TOKEN": "Podaj swój kod zaproszenia", 5 | "TITLE": "Dołącz do społeczności %s na Slacku!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Digite seu email abaixo para se juntar a %s no Slack!", 3 | "ENTER_EMAIL": "Insira seu endereço de e-mail", 4 | "ENTER_TOKEN": "Digite seu Token de convite que foi dado a você", 5 | "TITLE": "Junte-se a comunidade %s no Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Introduza o seu email para participar no Slack de %s!", 3 | "ENTER_EMAIL": "Insira o seu endereço de e-mail", 4 | "ENTER_TOKEN": "Digite o convite que lhe foi entregue", 5 | "TITLE": "Junte-se à comunidade Slack de %s!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Напишите свой E-Mail чтобы присоединиться к %s в Slack!", 3 | "ENTER_EMAIL": "Введите e-mail на него будет отправлено приглашение", 4 | "ENTER_TOKEN": "Введите ключ-приглашение", 5 | "TITLE": "Присоиденяйтесь к %s в Slack!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "Slack'te %s topluluğuna katılmak için e-mailinizi giriniz.", 3 | "ENTER_EMAIL": "E-mail adresinizi giriniz.", 4 | "ENTER_TOKEN": "Size verilen davet token'ınızı giriniz.", 5 | "TITLE": "Slack'te %s topluluğuna katılın" 6 | } 7 | -------------------------------------------------------------------------------- /locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "以下请输入您的电子邮箱加入 %s 的 Slack!", 3 | "ENTER_EMAIL": "输入您的电子邮箱地址", 4 | "ENTER_TOKEN": "输入提供予您的邀请令牌", 5 | "TITLE": "加入 %s 的 Slack 社群!" 6 | } 7 | -------------------------------------------------------------------------------- /locales/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "HEADER": "以下請輸入您的電子郵箱加入 %s 的 Slack!", 3 | "ENTER_EMAIL": "輸入您的電子郵箱地址", 4 | "ENTER_TOKEN": "輸入提供予您的邀請令牌", 5 | "TITLE": "加入 %s 的 Slack 社群!" 6 | } 7 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | applications: 2 | - name: slack-invite-automation 3 | host: slack-invite-automation 4 | command: bin/www 5 | instances: 1 6 | memory: 128M 7 | disk_quota: 128M 8 | env: 9 | # your community or team name to display on join page 10 | COMMUNITY_NAME: SocketIO 11 | 12 | # your slack team url (ex: socketio.slack.com) 13 | SLACK_URL: socketio.slack.com 14 | 15 | # access token of slack 16 | # You can generate it in https://api.slack.com/web#auth 17 | # You should generate the token in admin user, not owner. 18 | # If you generate the token in owner user, missing_scope error will occur. 19 | # 20 | # You can test your token via curl: 21 | # curl -X POST 'https://YOUR-SLACK-TEAM.slack.com/api/users.admin.invite' \ 22 | # --data 'email=EMAIL&token=TOKEN&set_active=true' \ 23 | # --compressed 24 | SLACK_TOKEN: 25 | 26 | # an optional security measure - if it is set, then that token will be required to get invited. 27 | # INVITE_TOKEN: 28 | 29 | LOCALE: en 30 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slack-invite-automation", 3 | "version": "0.3.4", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "abbrev": { 8 | "version": "1.1.1", 9 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 10 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 11 | }, 12 | "accepts": { 13 | "version": "1.3.5", 14 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 15 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 16 | "requires": { 17 | "mime-types": "~2.1.18", 18 | "negotiator": "0.6.1" 19 | } 20 | }, 21 | "acorn": { 22 | "version": "2.7.0", 23 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", 24 | "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" 25 | }, 26 | "ajv": { 27 | "version": "5.5.2", 28 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", 29 | "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", 30 | "requires": { 31 | "co": "^4.6.0", 32 | "fast-deep-equal": "^1.0.0", 33 | "fast-json-stable-stringify": "^2.0.0", 34 | "json-schema-traverse": "^0.3.0" 35 | } 36 | }, 37 | "align-text": { 38 | "version": "0.1.4", 39 | "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", 40 | "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", 41 | "requires": { 42 | "kind-of": "^3.0.2", 43 | "longest": "^1.0.1", 44 | "repeat-string": "^1.5.2" 45 | } 46 | }, 47 | "ambi": { 48 | "version": "2.5.0", 49 | "resolved": "https://registry.npmjs.org/ambi/-/ambi-2.5.0.tgz", 50 | "integrity": "sha1-fI43K+SIkRV+fOoBy2+RQ9H3QiA=", 51 | "requires": { 52 | "editions": "^1.1.1", 53 | "typechecker": "^4.3.0" 54 | }, 55 | "dependencies": { 56 | "typechecker": { 57 | "version": "4.5.0", 58 | "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-4.5.0.tgz", 59 | "integrity": "sha512-bqPE/ck3bVIaXP7gMKTKSHrypT32lpYTpiqzPYeYzdSQnmaGvaGhy7TnN/M/+5R+2rs/kKcp9ZLPRp/Q9Yj+4w==", 60 | "requires": { 61 | "editions": "^1.3.4" 62 | } 63 | } 64 | } 65 | }, 66 | "array-flatten": { 67 | "version": "1.1.1", 68 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 69 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 70 | }, 71 | "asn1": { 72 | "version": "0.2.3", 73 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", 74 | "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" 75 | }, 76 | "assert-plus": { 77 | "version": "1.0.0", 78 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 79 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 80 | }, 81 | "async": { 82 | "version": "1.5.2", 83 | "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", 84 | "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" 85 | }, 86 | "asynckit": { 87 | "version": "0.4.0", 88 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 89 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 90 | }, 91 | "aws-sign2": { 92 | "version": "0.7.0", 93 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 94 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 95 | }, 96 | "aws4": { 97 | "version": "1.7.0", 98 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", 99 | "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==" 100 | }, 101 | "balanced-match": { 102 | "version": "1.0.0", 103 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 104 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 105 | }, 106 | "basic-auth": { 107 | "version": "2.0.0", 108 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", 109 | "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", 110 | "requires": { 111 | "safe-buffer": "5.1.1" 112 | } 113 | }, 114 | "bcrypt-pbkdf": { 115 | "version": "1.0.2", 116 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 117 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 118 | "optional": true, 119 | "requires": { 120 | "tweetnacl": "^0.14.3" 121 | } 122 | }, 123 | "body-parser": { 124 | "version": "1.18.3", 125 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", 126 | "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", 127 | "requires": { 128 | "bytes": "3.0.0", 129 | "content-type": "~1.0.4", 130 | "debug": "2.6.9", 131 | "depd": "~1.1.2", 132 | "http-errors": "~1.6.3", 133 | "iconv-lite": "0.4.23", 134 | "on-finished": "~2.3.0", 135 | "qs": "6.5.2", 136 | "raw-body": "2.3.3", 137 | "type-is": "~1.6.16" 138 | }, 139 | "dependencies": { 140 | "debug": { 141 | "version": "2.6.9", 142 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 143 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 144 | "requires": { 145 | "ms": "2.0.0" 146 | } 147 | } 148 | } 149 | }, 150 | "brace-expansion": { 151 | "version": "1.1.11", 152 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 153 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 154 | "requires": { 155 | "balanced-match": "^1.0.0", 156 | "concat-map": "0.0.1" 157 | } 158 | }, 159 | "bytes": { 160 | "version": "3.0.0", 161 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 162 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 163 | }, 164 | "camelcase": { 165 | "version": "1.2.1", 166 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", 167 | "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" 168 | }, 169 | "caseless": { 170 | "version": "0.12.0", 171 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 172 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 173 | }, 174 | "center-align": { 175 | "version": "0.1.3", 176 | "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", 177 | "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", 178 | "requires": { 179 | "align-text": "^0.1.3", 180 | "lazy-cache": "^1.0.3" 181 | } 182 | }, 183 | "cliui": { 184 | "version": "2.1.0", 185 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", 186 | "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", 187 | "requires": { 188 | "center-align": "^0.1.1", 189 | "right-align": "^0.1.1", 190 | "wordwrap": "0.0.2" 191 | }, 192 | "dependencies": { 193 | "wordwrap": { 194 | "version": "0.0.2", 195 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", 196 | "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" 197 | } 198 | } 199 | }, 200 | "co": { 201 | "version": "4.6.0", 202 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 203 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 204 | }, 205 | "combined-stream": { 206 | "version": "1.0.6", 207 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", 208 | "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", 209 | "requires": { 210 | "delayed-stream": "~1.0.0" 211 | } 212 | }, 213 | "concat-map": { 214 | "version": "0.0.1", 215 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 216 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 217 | }, 218 | "constantinople": { 219 | "version": "3.0.2", 220 | "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz", 221 | "integrity": "sha1-S5RdmTeQe82Y7ldRIsOBdRZUQUE=", 222 | "requires": { 223 | "acorn": "^2.1.0" 224 | } 225 | }, 226 | "content-disposition": { 227 | "version": "0.5.2", 228 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 229 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 230 | }, 231 | "content-type": { 232 | "version": "1.0.4", 233 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 234 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 235 | }, 236 | "cookie": { 237 | "version": "0.3.1", 238 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 239 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 240 | }, 241 | "cookie-parser": { 242 | "version": "1.4.3", 243 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz", 244 | "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=", 245 | "requires": { 246 | "cookie": "0.3.1", 247 | "cookie-signature": "1.0.6" 248 | } 249 | }, 250 | "cookie-signature": { 251 | "version": "1.0.6", 252 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 253 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 254 | }, 255 | "core-util-is": { 256 | "version": "1.0.2", 257 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 258 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 259 | }, 260 | "csextends": { 261 | "version": "1.2.0", 262 | "resolved": "https://registry.npmjs.org/csextends/-/csextends-1.2.0.tgz", 263 | "integrity": "sha512-S/8k1bDTJIwuGgQYmsRoE+8P+ohV32WhQ0l4zqrc0XDdxOhjQQD7/wTZwCzoZX53jSX3V/qwjT+OkPTxWQcmjg==" 264 | }, 265 | "dashdash": { 266 | "version": "1.14.1", 267 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 268 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 269 | "requires": { 270 | "assert-plus": "^1.0.0" 271 | } 272 | }, 273 | "debug": { 274 | "version": "3.1.0", 275 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 276 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 277 | "requires": { 278 | "ms": "2.0.0" 279 | } 280 | }, 281 | "decamelize": { 282 | "version": "1.2.0", 283 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 284 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" 285 | }, 286 | "delayed-stream": { 287 | "version": "1.0.0", 288 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 289 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 290 | }, 291 | "depd": { 292 | "version": "1.1.2", 293 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 294 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 295 | }, 296 | "destroy": { 297 | "version": "1.0.4", 298 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 299 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 300 | }, 301 | "doctypes": { 302 | "version": "1.1.0", 303 | "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", 304 | "integrity": "sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=" 305 | }, 306 | "dotenv": { 307 | "version": "5.0.1", 308 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", 309 | "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==" 310 | }, 311 | "eachr": { 312 | "version": "2.0.4", 313 | "resolved": "https://registry.npmjs.org/eachr/-/eachr-2.0.4.tgz", 314 | "integrity": "sha1-Rm98qhBwj2EFCeMsgHqv5X/BIr8=", 315 | "requires": { 316 | "typechecker": "^2.0.8" 317 | } 318 | }, 319 | "ecc-jsbn": { 320 | "version": "0.1.1", 321 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", 322 | "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", 323 | "optional": true, 324 | "requires": { 325 | "jsbn": "~0.1.0" 326 | } 327 | }, 328 | "editions": { 329 | "version": "1.3.4", 330 | "resolved": "https://registry.npmjs.org/editions/-/editions-1.3.4.tgz", 331 | "integrity": "sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==" 332 | }, 333 | "ee-first": { 334 | "version": "1.1.1", 335 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 336 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 337 | }, 338 | "encodeurl": { 339 | "version": "1.0.2", 340 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 341 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 342 | }, 343 | "escape-html": { 344 | "version": "1.0.3", 345 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 346 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 347 | }, 348 | "etag": { 349 | "version": "1.8.1", 350 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 351 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 352 | }, 353 | "express": { 354 | "version": "4.16.3", 355 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", 356 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", 357 | "requires": { 358 | "accepts": "~1.3.5", 359 | "array-flatten": "1.1.1", 360 | "body-parser": "1.18.2", 361 | "content-disposition": "0.5.2", 362 | "content-type": "~1.0.4", 363 | "cookie": "0.3.1", 364 | "cookie-signature": "1.0.6", 365 | "debug": "2.6.9", 366 | "depd": "~1.1.2", 367 | "encodeurl": "~1.0.2", 368 | "escape-html": "~1.0.3", 369 | "etag": "~1.8.1", 370 | "finalhandler": "1.1.1", 371 | "fresh": "0.5.2", 372 | "merge-descriptors": "1.0.1", 373 | "methods": "~1.1.2", 374 | "on-finished": "~2.3.0", 375 | "parseurl": "~1.3.2", 376 | "path-to-regexp": "0.1.7", 377 | "proxy-addr": "~2.0.3", 378 | "qs": "6.5.1", 379 | "range-parser": "~1.2.0", 380 | "safe-buffer": "5.1.1", 381 | "send": "0.16.2", 382 | "serve-static": "1.13.2", 383 | "setprototypeof": "1.1.0", 384 | "statuses": "~1.4.0", 385 | "type-is": "~1.6.16", 386 | "utils-merge": "1.0.1", 387 | "vary": "~1.1.2" 388 | }, 389 | "dependencies": { 390 | "body-parser": { 391 | "version": "1.18.2", 392 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 393 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 394 | "requires": { 395 | "bytes": "3.0.0", 396 | "content-type": "~1.0.4", 397 | "debug": "2.6.9", 398 | "depd": "~1.1.1", 399 | "http-errors": "~1.6.2", 400 | "iconv-lite": "0.4.19", 401 | "on-finished": "~2.3.0", 402 | "qs": "6.5.1", 403 | "raw-body": "2.3.2", 404 | "type-is": "~1.6.15" 405 | } 406 | }, 407 | "debug": { 408 | "version": "2.6.9", 409 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 410 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 411 | "requires": { 412 | "ms": "2.0.0" 413 | } 414 | }, 415 | "iconv-lite": { 416 | "version": "0.4.19", 417 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 418 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 419 | }, 420 | "qs": { 421 | "version": "6.5.1", 422 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 423 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 424 | }, 425 | "raw-body": { 426 | "version": "2.3.2", 427 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 428 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 429 | "requires": { 430 | "bytes": "3.0.0", 431 | "http-errors": "1.6.2", 432 | "iconv-lite": "0.4.19", 433 | "unpipe": "1.0.0" 434 | }, 435 | "dependencies": { 436 | "depd": { 437 | "version": "1.1.1", 438 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 439 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 440 | }, 441 | "http-errors": { 442 | "version": "1.6.2", 443 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 444 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 445 | "requires": { 446 | "depd": "1.1.1", 447 | "inherits": "2.0.3", 448 | "setprototypeof": "1.0.3", 449 | "statuses": ">= 1.3.1 < 2" 450 | } 451 | }, 452 | "setprototypeof": { 453 | "version": "1.0.3", 454 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 455 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 456 | } 457 | } 458 | }, 459 | "statuses": { 460 | "version": "1.4.0", 461 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 462 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 463 | } 464 | } 465 | }, 466 | "extend": { 467 | "version": "3.0.1", 468 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", 469 | "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" 470 | }, 471 | "extendr": { 472 | "version": "2.1.0", 473 | "resolved": "https://registry.npmjs.org/extendr/-/extendr-2.1.0.tgz", 474 | "integrity": "sha1-MBqgu+pWX00tyPVw8qImEahSe1Y=", 475 | "requires": { 476 | "typechecker": "~2.0.1" 477 | }, 478 | "dependencies": { 479 | "typechecker": { 480 | "version": "2.0.8", 481 | "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-2.0.8.tgz", 482 | "integrity": "sha1-6D2oS7ZMWEzLNFg4V2xAsDN9uC4=" 483 | } 484 | } 485 | }, 486 | "extract-opts": { 487 | "version": "2.2.0", 488 | "resolved": "https://registry.npmjs.org/extract-opts/-/extract-opts-2.2.0.tgz", 489 | "integrity": "sha1-H6KOunNSxttID4hc63GkaBC+bX0=", 490 | "requires": { 491 | "typechecker": "~2.0.1" 492 | }, 493 | "dependencies": { 494 | "typechecker": { 495 | "version": "2.0.8", 496 | "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-2.0.8.tgz", 497 | "integrity": "sha1-6D2oS7ZMWEzLNFg4V2xAsDN9uC4=" 498 | } 499 | } 500 | }, 501 | "extsprintf": { 502 | "version": "1.3.0", 503 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 504 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 505 | }, 506 | "fast-deep-equal": { 507 | "version": "1.1.0", 508 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", 509 | "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" 510 | }, 511 | "fast-json-stable-stringify": { 512 | "version": "2.0.0", 513 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 514 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 515 | }, 516 | "finalhandler": { 517 | "version": "1.1.1", 518 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 519 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 520 | "requires": { 521 | "debug": "2.6.9", 522 | "encodeurl": "~1.0.2", 523 | "escape-html": "~1.0.3", 524 | "on-finished": "~2.3.0", 525 | "parseurl": "~1.3.2", 526 | "statuses": "~1.4.0", 527 | "unpipe": "~1.0.0" 528 | }, 529 | "dependencies": { 530 | "debug": { 531 | "version": "2.6.9", 532 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 533 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 534 | "requires": { 535 | "ms": "2.0.0" 536 | } 537 | }, 538 | "statuses": { 539 | "version": "1.4.0", 540 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 541 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 542 | } 543 | } 544 | }, 545 | "forever-agent": { 546 | "version": "0.6.1", 547 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 548 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 549 | }, 550 | "form-data": { 551 | "version": "2.3.2", 552 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", 553 | "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", 554 | "requires": { 555 | "asynckit": "^0.4.0", 556 | "combined-stream": "1.0.6", 557 | "mime-types": "^2.1.12" 558 | } 559 | }, 560 | "forwarded": { 561 | "version": "0.1.2", 562 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 563 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 564 | }, 565 | "fresh": { 566 | "version": "0.5.2", 567 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 568 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 569 | }, 570 | "function-bind": { 571 | "version": "1.1.1", 572 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 573 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 574 | }, 575 | "getpass": { 576 | "version": "0.1.7", 577 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 578 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 579 | "requires": { 580 | "assert-plus": "^1.0.0" 581 | } 582 | }, 583 | "glob": { 584 | "version": "6.0.4", 585 | "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", 586 | "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", 587 | "requires": { 588 | "inflight": "^1.0.4", 589 | "inherits": "2", 590 | "minimatch": "2 || 3", 591 | "once": "^1.3.0", 592 | "path-is-absolute": "^1.0.0" 593 | } 594 | }, 595 | "graceful-fs": { 596 | "version": "4.1.11", 597 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", 598 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" 599 | }, 600 | "har-schema": { 601 | "version": "2.0.0", 602 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 603 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 604 | }, 605 | "har-validator": { 606 | "version": "5.0.3", 607 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", 608 | "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", 609 | "requires": { 610 | "ajv": "^5.1.0", 611 | "har-schema": "^2.0.0" 612 | } 613 | }, 614 | "has": { 615 | "version": "1.0.3", 616 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 617 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 618 | "requires": { 619 | "function-bind": "^1.1.1" 620 | } 621 | }, 622 | "http-errors": { 623 | "version": "1.6.3", 624 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 625 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 626 | "requires": { 627 | "depd": "~1.1.2", 628 | "inherits": "2.0.3", 629 | "setprototypeof": "1.1.0", 630 | "statuses": ">= 1.4.0 < 2" 631 | } 632 | }, 633 | "http-signature": { 634 | "version": "1.2.0", 635 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 636 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 637 | "requires": { 638 | "assert-plus": "^1.0.0", 639 | "jsprim": "^1.2.2", 640 | "sshpk": "^1.7.0" 641 | } 642 | }, 643 | "i18n": { 644 | "version": "0.8.3", 645 | "resolved": "https://registry.npmjs.org/i18n/-/i18n-0.8.3.tgz", 646 | "integrity": "sha1-LYzxwkciYCwgQdAbpq5eqlE4jw4=", 647 | "requires": { 648 | "debug": "*", 649 | "make-plural": "^3.0.3", 650 | "math-interval-parser": "^1.1.0", 651 | "messageformat": "^0.3.1", 652 | "mustache": "*", 653 | "sprintf-js": ">=1.0.3" 654 | } 655 | }, 656 | "iconv-lite": { 657 | "version": "0.4.23", 658 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", 659 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", 660 | "requires": { 661 | "safer-buffer": ">= 2.1.2 < 3" 662 | } 663 | }, 664 | "ignorefs": { 665 | "version": "1.2.0", 666 | "resolved": "https://registry.npmjs.org/ignorefs/-/ignorefs-1.2.0.tgz", 667 | "integrity": "sha1-2ln7hYl25KXkNwLM0fKC/byeV1Y=", 668 | "requires": { 669 | "editions": "^1.3.3", 670 | "ignorepatterns": "^1.1.0" 671 | } 672 | }, 673 | "ignorepatterns": { 674 | "version": "1.1.0", 675 | "resolved": "https://registry.npmjs.org/ignorepatterns/-/ignorepatterns-1.1.0.tgz", 676 | "integrity": "sha1-rI9DbyI5td+2bV8NOpBKh6xnzF4=" 677 | }, 678 | "inflight": { 679 | "version": "1.0.6", 680 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 681 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 682 | "requires": { 683 | "once": "^1.3.0", 684 | "wrappy": "1" 685 | } 686 | }, 687 | "inherits": { 688 | "version": "2.0.3", 689 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 690 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 691 | }, 692 | "ipaddr.js": { 693 | "version": "1.6.0", 694 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", 695 | "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" 696 | }, 697 | "is-buffer": { 698 | "version": "1.1.6", 699 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 700 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 701 | }, 702 | "is-expression": { 703 | "version": "3.0.0", 704 | "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", 705 | "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", 706 | "requires": { 707 | "acorn": "~4.0.2", 708 | "object-assign": "^4.0.1" 709 | }, 710 | "dependencies": { 711 | "acorn": { 712 | "version": "4.0.13", 713 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", 714 | "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" 715 | } 716 | } 717 | }, 718 | "is-promise": { 719 | "version": "2.1.0", 720 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 721 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" 722 | }, 723 | "is-regex": { 724 | "version": "1.0.4", 725 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 726 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 727 | "requires": { 728 | "has": "^1.0.1" 729 | } 730 | }, 731 | "is-typedarray": { 732 | "version": "1.0.0", 733 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 734 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 735 | }, 736 | "isstream": { 737 | "version": "0.1.2", 738 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 739 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 740 | }, 741 | "js-stringify": { 742 | "version": "1.0.2", 743 | "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", 744 | "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=" 745 | }, 746 | "jsbn": { 747 | "version": "0.1.1", 748 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 749 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 750 | "optional": true 751 | }, 752 | "json-schema": { 753 | "version": "0.2.3", 754 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 755 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 756 | }, 757 | "json-schema-traverse": { 758 | "version": "0.3.1", 759 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", 760 | "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" 761 | }, 762 | "json-stringify-safe": { 763 | "version": "5.0.1", 764 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 765 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 766 | }, 767 | "jsprim": { 768 | "version": "1.4.1", 769 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 770 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 771 | "requires": { 772 | "assert-plus": "1.0.0", 773 | "extsprintf": "1.3.0", 774 | "json-schema": "0.2.3", 775 | "verror": "1.10.0" 776 | } 777 | }, 778 | "kind-of": { 779 | "version": "3.2.2", 780 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 781 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 782 | "requires": { 783 | "is-buffer": "^1.1.5" 784 | } 785 | }, 786 | "lazy-cache": { 787 | "version": "1.0.4", 788 | "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", 789 | "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" 790 | }, 791 | "lodash": { 792 | "version": "4.17.10", 793 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", 794 | "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" 795 | }, 796 | "longest": { 797 | "version": "1.0.1", 798 | "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", 799 | "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" 800 | }, 801 | "make-plural": { 802 | "version": "3.0.6", 803 | "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-3.0.6.tgz", 804 | "integrity": "sha1-IDOgO6wpC487uRJY9lud9+iwHKc=", 805 | "requires": { 806 | "minimist": "^1.2.0" 807 | } 808 | }, 809 | "math-interval-parser": { 810 | "version": "1.1.0", 811 | "resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-1.1.0.tgz", 812 | "integrity": "sha1-2+2lsGsySZc8bfYXD94jhvCv2JM=", 813 | "requires": { 814 | "xregexp": "^2.0.0" 815 | } 816 | }, 817 | "media-typer": { 818 | "version": "0.3.0", 819 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 820 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 821 | }, 822 | "merge-descriptors": { 823 | "version": "1.0.1", 824 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 825 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 826 | }, 827 | "messageformat": { 828 | "version": "0.3.1", 829 | "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-0.3.1.tgz", 830 | "integrity": "sha1-5Y//gkXps5cXmeW0PbWLPpQX9aI=", 831 | "requires": { 832 | "async": "~1.5.2", 833 | "glob": "~6.0.4", 834 | "make-plural": "~3.0.3", 835 | "nopt": "~3.0.6", 836 | "watchr": "~2.4.13" 837 | } 838 | }, 839 | "methods": { 840 | "version": "1.1.2", 841 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 842 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 843 | }, 844 | "mime": { 845 | "version": "1.4.1", 846 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 847 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 848 | }, 849 | "mime-db": { 850 | "version": "1.33.0", 851 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 852 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 853 | }, 854 | "mime-types": { 855 | "version": "2.1.18", 856 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 857 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 858 | "requires": { 859 | "mime-db": "~1.33.0" 860 | } 861 | }, 862 | "minimatch": { 863 | "version": "3.0.4", 864 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 865 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 866 | "requires": { 867 | "brace-expansion": "^1.1.7" 868 | } 869 | }, 870 | "minimist": { 871 | "version": "1.2.0", 872 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", 873 | "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", 874 | "optional": true 875 | }, 876 | "morgan": { 877 | "version": "1.9.0", 878 | "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", 879 | "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", 880 | "requires": { 881 | "basic-auth": "~2.0.0", 882 | "debug": "2.6.9", 883 | "depd": "~1.1.1", 884 | "on-finished": "~2.3.0", 885 | "on-headers": "~1.0.1" 886 | }, 887 | "dependencies": { 888 | "debug": { 889 | "version": "2.6.9", 890 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 891 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 892 | "requires": { 893 | "ms": "2.0.0" 894 | } 895 | } 896 | } 897 | }, 898 | "ms": { 899 | "version": "2.0.0", 900 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 901 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 902 | }, 903 | "mustache": { 904 | "version": "2.3.0", 905 | "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.0.tgz", 906 | "integrity": "sha1-QCj3d4sXcIpImTCm5SrDvKDaQdA=" 907 | }, 908 | "negotiator": { 909 | "version": "0.6.1", 910 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 911 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 912 | }, 913 | "nopt": { 914 | "version": "3.0.6", 915 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", 916 | "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", 917 | "requires": { 918 | "abbrev": "1" 919 | } 920 | }, 921 | "oauth-sign": { 922 | "version": "0.8.2", 923 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", 924 | "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" 925 | }, 926 | "object-assign": { 927 | "version": "4.1.1", 928 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 929 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 930 | }, 931 | "on-finished": { 932 | "version": "2.3.0", 933 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 934 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 935 | "requires": { 936 | "ee-first": "1.1.1" 937 | } 938 | }, 939 | "on-headers": { 940 | "version": "1.0.1", 941 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", 942 | "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" 943 | }, 944 | "once": { 945 | "version": "1.4.0", 946 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 947 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 948 | "requires": { 949 | "wrappy": "1" 950 | } 951 | }, 952 | "parseurl": { 953 | "version": "1.3.2", 954 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 955 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 956 | }, 957 | "path-is-absolute": { 958 | "version": "1.0.1", 959 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 960 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 961 | }, 962 | "path-parse": { 963 | "version": "1.0.5", 964 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 965 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" 966 | }, 967 | "path-to-regexp": { 968 | "version": "0.1.7", 969 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 970 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 971 | }, 972 | "performance-now": { 973 | "version": "2.1.0", 974 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 975 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 976 | }, 977 | "proxy-addr": { 978 | "version": "2.0.3", 979 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", 980 | "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", 981 | "requires": { 982 | "forwarded": "~0.1.2", 983 | "ipaddr.js": "1.6.0" 984 | } 985 | }, 986 | "pug": { 987 | "version": "2.0.3", 988 | "resolved": "https://registry.npmjs.org/pug/-/pug-2.0.3.tgz", 989 | "integrity": "sha1-ccuoJTfJWl6rftBGluQiH1Oqh44=", 990 | "requires": { 991 | "pug-code-gen": "^2.0.1", 992 | "pug-filters": "^3.1.0", 993 | "pug-lexer": "^4.0.0", 994 | "pug-linker": "^3.0.5", 995 | "pug-load": "^2.0.11", 996 | "pug-parser": "^5.0.0", 997 | "pug-runtime": "^2.0.4", 998 | "pug-strip-comments": "^1.0.3" 999 | } 1000 | }, 1001 | "pug-attrs": { 1002 | "version": "2.0.3", 1003 | "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-2.0.3.tgz", 1004 | "integrity": "sha1-owlflw5kFR972tlX7vVftdeQXRU=", 1005 | "requires": { 1006 | "constantinople": "^3.0.1", 1007 | "js-stringify": "^1.0.1", 1008 | "pug-runtime": "^2.0.4" 1009 | } 1010 | }, 1011 | "pug-code-gen": { 1012 | "version": "2.0.1", 1013 | "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-2.0.1.tgz", 1014 | "integrity": "sha1-CVHsgyJddNjPxHan+Zolm199BQw=", 1015 | "requires": { 1016 | "constantinople": "^3.0.1", 1017 | "doctypes": "^1.1.0", 1018 | "js-stringify": "^1.0.1", 1019 | "pug-attrs": "^2.0.3", 1020 | "pug-error": "^1.3.2", 1021 | "pug-runtime": "^2.0.4", 1022 | "void-elements": "^2.0.1", 1023 | "with": "^5.0.0" 1024 | }, 1025 | "dependencies": { 1026 | "acorn": { 1027 | "version": "3.3.0", 1028 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", 1029 | "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" 1030 | }, 1031 | "acorn-globals": { 1032 | "version": "3.1.0", 1033 | "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-3.1.0.tgz", 1034 | "integrity": "sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=", 1035 | "requires": { 1036 | "acorn": "^4.0.4" 1037 | }, 1038 | "dependencies": { 1039 | "acorn": { 1040 | "version": "4.0.13", 1041 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", 1042 | "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" 1043 | } 1044 | } 1045 | }, 1046 | "with": { 1047 | "version": "5.1.1", 1048 | "resolved": "https://registry.npmjs.org/with/-/with-5.1.1.tgz", 1049 | "integrity": "sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=", 1050 | "requires": { 1051 | "acorn": "^3.1.0", 1052 | "acorn-globals": "^3.0.0" 1053 | } 1054 | } 1055 | } 1056 | }, 1057 | "pug-error": { 1058 | "version": "1.3.2", 1059 | "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.2.tgz", 1060 | "integrity": "sha1-U659nSm7A89WRJOgJhCfVMR/XyY=" 1061 | }, 1062 | "pug-filters": { 1063 | "version": "3.1.0", 1064 | "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.1.0.tgz", 1065 | "integrity": "sha1-JxZVVbwEwjbkqisDZiRt+gIbYm4=", 1066 | "requires": { 1067 | "clean-css": "^4.1.11", 1068 | "constantinople": "^3.0.1", 1069 | "jstransformer": "1.0.0", 1070 | "pug-error": "^1.3.2", 1071 | "pug-walk": "^1.1.7", 1072 | "resolve": "^1.1.6", 1073 | "uglify-js": "^2.6.1" 1074 | }, 1075 | "dependencies": { 1076 | "asap": { 1077 | "version": "2.0.6", 1078 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 1079 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" 1080 | }, 1081 | "clean-css": { 1082 | "version": "4.1.11", 1083 | "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.11.tgz", 1084 | "integrity": "sha1-Ls3xRaujj1R0DybO/Q/z4D4SXWo=", 1085 | "requires": { 1086 | "source-map": "0.5.x" 1087 | } 1088 | }, 1089 | "jstransformer": { 1090 | "version": "1.0.0", 1091 | "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", 1092 | "integrity": "sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=", 1093 | "requires": { 1094 | "is-promise": "^2.0.0", 1095 | "promise": "^7.0.1" 1096 | } 1097 | }, 1098 | "promise": { 1099 | "version": "7.3.1", 1100 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", 1101 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", 1102 | "requires": { 1103 | "asap": "~2.0.3" 1104 | } 1105 | }, 1106 | "source-map": { 1107 | "version": "0.5.7", 1108 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 1109 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" 1110 | } 1111 | } 1112 | }, 1113 | "pug-lexer": { 1114 | "version": "4.0.0", 1115 | "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.0.0.tgz", 1116 | "integrity": "sha1-IQwYRX7y4XYCQnQMXmR715TOwng=", 1117 | "requires": { 1118 | "character-parser": "^2.1.1", 1119 | "is-expression": "^3.0.0", 1120 | "pug-error": "^1.3.2" 1121 | }, 1122 | "dependencies": { 1123 | "character-parser": { 1124 | "version": "2.2.0", 1125 | "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", 1126 | "integrity": "sha1-x84o821LzZdE5f/CxfzeHHMmH8A=", 1127 | "requires": { 1128 | "is-regex": "^1.0.3" 1129 | } 1130 | } 1131 | } 1132 | }, 1133 | "pug-linker": { 1134 | "version": "3.0.5", 1135 | "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-3.0.5.tgz", 1136 | "integrity": "sha1-npp65ABWgtAn3uuWsAD4juuDoC8=", 1137 | "requires": { 1138 | "pug-error": "^1.3.2", 1139 | "pug-walk": "^1.1.7" 1140 | } 1141 | }, 1142 | "pug-load": { 1143 | "version": "2.0.11", 1144 | "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.11.tgz", 1145 | "integrity": "sha1-5kjlftET/iwfRdV4WOorrWvAFSc=", 1146 | "requires": { 1147 | "object-assign": "^4.1.0", 1148 | "pug-walk": "^1.1.7" 1149 | } 1150 | }, 1151 | "pug-parser": { 1152 | "version": "5.0.0", 1153 | "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.0.tgz", 1154 | "integrity": "sha1-45Stmz/KkxI5QK/4hcBuRKt+aOQ=", 1155 | "requires": { 1156 | "pug-error": "^1.3.2", 1157 | "token-stream": "0.0.1" 1158 | } 1159 | }, 1160 | "pug-runtime": { 1161 | "version": "2.0.4", 1162 | "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.4.tgz", 1163 | "integrity": "sha1-4XjhvaaKsujArPybztLFT9iM61g=" 1164 | }, 1165 | "pug-strip-comments": { 1166 | "version": "1.0.3", 1167 | "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.3.tgz", 1168 | "integrity": "sha1-8VWVkiBu3G+FMQ2s9K+0igJa9Z8=", 1169 | "requires": { 1170 | "pug-error": "^1.3.2" 1171 | } 1172 | }, 1173 | "pug-walk": { 1174 | "version": "1.1.7", 1175 | "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.7.tgz", 1176 | "integrity": "sha1-wA1cUSi6xYBr7BXSt+fNq+QlMfM=" 1177 | }, 1178 | "punycode": { 1179 | "version": "1.4.1", 1180 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 1181 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 1182 | }, 1183 | "qs": { 1184 | "version": "6.5.2", 1185 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 1186 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 1187 | }, 1188 | "range-parser": { 1189 | "version": "1.2.0", 1190 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 1191 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 1192 | }, 1193 | "raw-body": { 1194 | "version": "2.3.3", 1195 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", 1196 | "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", 1197 | "requires": { 1198 | "bytes": "3.0.0", 1199 | "http-errors": "1.6.3", 1200 | "iconv-lite": "0.4.23", 1201 | "unpipe": "1.0.0" 1202 | } 1203 | }, 1204 | "repeat-string": { 1205 | "version": "1.6.1", 1206 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 1207 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" 1208 | }, 1209 | "request": { 1210 | "version": "2.87.0", 1211 | "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", 1212 | "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", 1213 | "requires": { 1214 | "aws-sign2": "~0.7.0", 1215 | "aws4": "^1.6.0", 1216 | "caseless": "~0.12.0", 1217 | "combined-stream": "~1.0.5", 1218 | "extend": "~3.0.1", 1219 | "forever-agent": "~0.6.1", 1220 | "form-data": "~2.3.1", 1221 | "har-validator": "~5.0.3", 1222 | "http-signature": "~1.2.0", 1223 | "is-typedarray": "~1.0.0", 1224 | "isstream": "~0.1.2", 1225 | "json-stringify-safe": "~5.0.1", 1226 | "mime-types": "~2.1.17", 1227 | "oauth-sign": "~0.8.2", 1228 | "performance-now": "^2.1.0", 1229 | "qs": "~6.5.1", 1230 | "safe-buffer": "^5.1.1", 1231 | "tough-cookie": "~2.3.3", 1232 | "tunnel-agent": "^0.6.0", 1233 | "uuid": "^3.1.0" 1234 | } 1235 | }, 1236 | "resolve": { 1237 | "version": "1.8.1", 1238 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", 1239 | "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", 1240 | "requires": { 1241 | "path-parse": "^1.0.5" 1242 | } 1243 | }, 1244 | "right-align": { 1245 | "version": "0.1.3", 1246 | "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", 1247 | "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", 1248 | "requires": { 1249 | "align-text": "^0.1.1" 1250 | } 1251 | }, 1252 | "safe-buffer": { 1253 | "version": "5.1.1", 1254 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 1255 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 1256 | }, 1257 | "safefs": { 1258 | "version": "3.2.2", 1259 | "resolved": "https://registry.npmjs.org/safefs/-/safefs-3.2.2.tgz", 1260 | "integrity": "sha1-gXDBRE1wOOCMrqBaN0+uL6NJ4Vw=", 1261 | "requires": { 1262 | "graceful-fs": "*" 1263 | } 1264 | }, 1265 | "safer-buffer": { 1266 | "version": "2.1.2", 1267 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1268 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1269 | }, 1270 | "sanitize": { 1271 | "version": "2.1.0", 1272 | "resolved": "https://registry.npmjs.org/sanitize/-/sanitize-2.1.0.tgz", 1273 | "integrity": "sha512-HLDVriFJnrm6ElDe2E8alAKDMZGMtM8CdKhvunp9592j8hNwZmmsmhk/t6WZbWonKJsHK0OoxH5S1Yoie4sSpw==", 1274 | "requires": { 1275 | "lodash": "^4.17.0", 1276 | "validator": "^3.33.0" 1277 | } 1278 | }, 1279 | "scandirectory": { 1280 | "version": "2.5.0", 1281 | "resolved": "https://registry.npmjs.org/scandirectory/-/scandirectory-2.5.0.tgz", 1282 | "integrity": "sha1-bOA/VKCQtmjjy+2/IO354xBZPnI=", 1283 | "requires": { 1284 | "ignorefs": "^1.0.0", 1285 | "safefs": "^3.1.2", 1286 | "taskgroup": "^4.0.5" 1287 | } 1288 | }, 1289 | "send": { 1290 | "version": "0.16.2", 1291 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 1292 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 1293 | "requires": { 1294 | "debug": "2.6.9", 1295 | "depd": "~1.1.2", 1296 | "destroy": "~1.0.4", 1297 | "encodeurl": "~1.0.2", 1298 | "escape-html": "~1.0.3", 1299 | "etag": "~1.8.1", 1300 | "fresh": "0.5.2", 1301 | "http-errors": "~1.6.2", 1302 | "mime": "1.4.1", 1303 | "ms": "2.0.0", 1304 | "on-finished": "~2.3.0", 1305 | "range-parser": "~1.2.0", 1306 | "statuses": "~1.4.0" 1307 | }, 1308 | "dependencies": { 1309 | "debug": { 1310 | "version": "2.6.9", 1311 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 1312 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 1313 | "requires": { 1314 | "ms": "2.0.0" 1315 | } 1316 | }, 1317 | "statuses": { 1318 | "version": "1.4.0", 1319 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 1320 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 1321 | } 1322 | } 1323 | }, 1324 | "serve-favicon": { 1325 | "version": "2.5.0", 1326 | "resolved": "https://registry.npmjs.org/serve-favicon/-/serve-favicon-2.5.0.tgz", 1327 | "integrity": "sha1-k10kDN/g9YBTB/3+ln2IlCosvPA=", 1328 | "requires": { 1329 | "etag": "~1.8.1", 1330 | "fresh": "0.5.2", 1331 | "ms": "2.1.1", 1332 | "parseurl": "~1.3.2", 1333 | "safe-buffer": "5.1.1" 1334 | }, 1335 | "dependencies": { 1336 | "ms": { 1337 | "version": "2.1.1", 1338 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 1339 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 1340 | } 1341 | } 1342 | }, 1343 | "serve-static": { 1344 | "version": "1.13.2", 1345 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 1346 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 1347 | "requires": { 1348 | "encodeurl": "~1.0.2", 1349 | "escape-html": "~1.0.3", 1350 | "parseurl": "~1.3.2", 1351 | "send": "0.16.2" 1352 | } 1353 | }, 1354 | "setprototypeof": { 1355 | "version": "1.1.0", 1356 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 1357 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 1358 | }, 1359 | "sprintf-js": { 1360 | "version": "1.1.1", 1361 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", 1362 | "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" 1363 | }, 1364 | "sshpk": { 1365 | "version": "1.14.2", 1366 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", 1367 | "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", 1368 | "requires": { 1369 | "asn1": "~0.2.3", 1370 | "assert-plus": "^1.0.0", 1371 | "bcrypt-pbkdf": "^1.0.0", 1372 | "dashdash": "^1.12.0", 1373 | "ecc-jsbn": "~0.1.1", 1374 | "getpass": "^0.1.1", 1375 | "jsbn": "~0.1.0", 1376 | "safer-buffer": "^2.0.2", 1377 | "tweetnacl": "~0.14.0" 1378 | } 1379 | }, 1380 | "statuses": { 1381 | "version": "1.5.0", 1382 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1383 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1384 | }, 1385 | "taskgroup": { 1386 | "version": "4.3.1", 1387 | "resolved": "https://registry.npmjs.org/taskgroup/-/taskgroup-4.3.1.tgz", 1388 | "integrity": "sha1-feGT/r12gnPEV3MElwJNUSwnkVo=", 1389 | "requires": { 1390 | "ambi": "^2.2.0", 1391 | "csextends": "^1.0.3" 1392 | } 1393 | }, 1394 | "token-stream": { 1395 | "version": "0.0.1", 1396 | "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-0.0.1.tgz", 1397 | "integrity": "sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=" 1398 | }, 1399 | "tough-cookie": { 1400 | "version": "2.3.4", 1401 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", 1402 | "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", 1403 | "requires": { 1404 | "punycode": "^1.4.1" 1405 | } 1406 | }, 1407 | "tunnel-agent": { 1408 | "version": "0.6.0", 1409 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1410 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1411 | "requires": { 1412 | "safe-buffer": "^5.0.1" 1413 | } 1414 | }, 1415 | "tweetnacl": { 1416 | "version": "0.14.5", 1417 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1418 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 1419 | "optional": true 1420 | }, 1421 | "type-is": { 1422 | "version": "1.6.16", 1423 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 1424 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 1425 | "requires": { 1426 | "media-typer": "0.3.0", 1427 | "mime-types": "~2.1.18" 1428 | } 1429 | }, 1430 | "typechecker": { 1431 | "version": "2.1.0", 1432 | "resolved": "https://registry.npmjs.org/typechecker/-/typechecker-2.1.0.tgz", 1433 | "integrity": "sha1-0cIJOlT/ihn1jP+HfuqlTyJC04M=" 1434 | }, 1435 | "uglify-js": { 1436 | "version": "2.8.29", 1437 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", 1438 | "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", 1439 | "requires": { 1440 | "source-map": "~0.5.1", 1441 | "uglify-to-browserify": "~1.0.0", 1442 | "yargs": "~3.10.0" 1443 | }, 1444 | "dependencies": { 1445 | "source-map": { 1446 | "version": "0.5.7", 1447 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 1448 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" 1449 | } 1450 | } 1451 | }, 1452 | "uglify-to-browserify": { 1453 | "version": "1.0.2", 1454 | "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", 1455 | "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", 1456 | "optional": true 1457 | }, 1458 | "unpipe": { 1459 | "version": "1.0.0", 1460 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1461 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1462 | }, 1463 | "utils-merge": { 1464 | "version": "1.0.1", 1465 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1466 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1467 | }, 1468 | "uuid": { 1469 | "version": "3.3.2", 1470 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 1471 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 1472 | }, 1473 | "validator": { 1474 | "version": "3.43.0", 1475 | "resolved": "https://registry.npmjs.org/validator/-/validator-3.43.0.tgz", 1476 | "integrity": "sha1-lkZLmS1BloM9l6GUv0Cxn/VLrgU=" 1477 | }, 1478 | "vary": { 1479 | "version": "1.1.2", 1480 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1481 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1482 | }, 1483 | "verror": { 1484 | "version": "1.10.0", 1485 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1486 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1487 | "requires": { 1488 | "assert-plus": "^1.0.0", 1489 | "core-util-is": "1.0.2", 1490 | "extsprintf": "^1.2.0" 1491 | } 1492 | }, 1493 | "void-elements": { 1494 | "version": "2.0.1", 1495 | "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", 1496 | "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" 1497 | }, 1498 | "watchr": { 1499 | "version": "2.4.13", 1500 | "resolved": "https://registry.npmjs.org/watchr/-/watchr-2.4.13.tgz", 1501 | "integrity": "sha1-10hHu01vkPYf4sdPn2hmKqDgdgE=", 1502 | "requires": { 1503 | "eachr": "^2.0.2", 1504 | "extendr": "^2.1.0", 1505 | "extract-opts": "^2.2.0", 1506 | "ignorefs": "^1.0.0", 1507 | "safefs": "^3.1.2", 1508 | "scandirectory": "^2.5.0", 1509 | "taskgroup": "^4.2.0", 1510 | "typechecker": "^2.0.8" 1511 | } 1512 | }, 1513 | "window-size": { 1514 | "version": "0.1.0", 1515 | "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", 1516 | "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" 1517 | }, 1518 | "wrappy": { 1519 | "version": "1.0.2", 1520 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1521 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 1522 | }, 1523 | "xregexp": { 1524 | "version": "2.0.0", 1525 | "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", 1526 | "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=" 1527 | }, 1528 | "yargs": { 1529 | "version": "3.10.0", 1530 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", 1531 | "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", 1532 | "requires": { 1533 | "camelcase": "^1.0.2", 1534 | "cliui": "^2.1.0", 1535 | "decamelize": "^1.0.0", 1536 | "window-size": "0.1.0" 1537 | } 1538 | } 1539 | } 1540 | } 1541 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slack-invite-automation", 3 | "version": "0.3.4", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "^1.18.0", 10 | "cookie-parser": "^1.4.0", 11 | "debug": "^3.1.0", 12 | "dotenv": "^5.0.1", 13 | "express": "^4.13.0", 14 | "i18n": "^0.8.3", 15 | "morgan": "^1.6.0", 16 | "pug": "^2.0.0-rc.4", 17 | "request": "^2.62.0", 18 | "serve-favicon": "^2.3.0", 19 | "sanitize": "^2.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* Reset */ 2 | 3 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,dfn,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;} 4 | 5 | 6 | html,body{ 7 | margin:0; 8 | padding:0; 9 | font-family: 'Open Sans', sans-serif; 10 | background: #fff url(../images/bg.jpg) center top no-repeat; 11 | background-size: cover; 12 | height: 100%; 13 | width: 100%; 14 | } 15 | 16 | 17 | #wrapper{ 18 | max-width: 100%; 19 | width: 940px; 20 | margin: 0 auto; 21 | } 22 | 23 | .main{ 24 | max-width: 100%; 25 | width: 940px; 26 | float: left; 27 | } 28 | 29 | .header, .content, .bottom, .sep2{ 30 | width: 100%; 31 | float: left; 32 | } 33 | 34 | 35 | 36 | .header{ 37 | margin-top: 160px; 38 | margin-bottom: 40px; 39 | } 40 | 41 | 42 | h1{ 43 | font-family: 'Open Sans', sans-serif; 44 | color: #fff; 45 | text-align: center; 46 | font-size: 50px; 47 | font-weight: 300; 48 | margin: 0 0 10px 0; 49 | } 50 | 51 | 52 | h1 strong{ 53 | font-size: 38px; 54 | font-weight: 700; 55 | } 56 | 57 | h2{ 58 | text-align: center; 59 | font-weight: 300; 60 | font-size: 20px; 61 | color: #ffffff; 62 | 63 | } 64 | 65 | h2 strong{ 66 | font-weight: 700; 67 | font-style: italic; 68 | } 69 | 70 | 71 | .information{ 72 | width: 480px; 73 | padding-top: 35px; 74 | margin: 0 auto; 75 | } 76 | 77 | h3{ 78 | font-size: 28px; 79 | font-weight: 600; 80 | color: #ffffff; 81 | } 82 | 83 | .information p{ 84 | font-size: 16px; 85 | color: #ffffff; 86 | display: block; 87 | } 88 | 89 | .form{ 90 | position: relative; 91 | width: 478px; 92 | margin-top: 20px; 93 | } 94 | 95 | .field{ 96 | background: url(../images/field.png) repeat; 97 | width: 448px; 98 | -webkit-border-radius: 30px;-moz-border-radius: 30px;border-radius: 30px; 99 | border: none; 100 | font-style: italic; 101 | font-family: 'Lato', sans-serif; 102 | font-size: 16px; 103 | color: #ffffff; 104 | padding: 15px; 105 | margin-bottom: 15px; 106 | outline: none; 107 | } 108 | .field:focus { 109 | border: 1px solid #ffffff; 110 | padding: 14px; 111 | } 112 | @media only screen and (max-width: 480px) { 113 | .information{ 114 | width: 100%; 115 | } 116 | .form{ 117 | width: 90%; 118 | margin-left: 5%; 119 | margin-right: 5%; 120 | } 121 | 122 | .field { 123 | width: 90%; 124 | } 125 | } 126 | 127 | .submit{ 128 | position: absolute; 129 | right: 15px; 130 | top: 13px; 131 | padding: 3px 10px; 132 | -webkit-border-radius: 30px;-moz-border-radius: 30px;border-radius: 30px; 133 | background: #ffffff; 134 | border: none; 135 | font-family: 'Lato', sans-serif; 136 | font-size: 14px; 137 | font-weight: bold; 138 | cursor: pointer; 139 | } 140 | 141 | 142 | .submit:hover{ 143 | background: #eeeeee; 144 | } 145 | 146 | .error { 147 | color: #FE7070; 148 | font-weight: 700; 149 | font-size: 25px; 150 | } 151 | 152 | a { 153 | color: #92BCF2; 154 | font-weight: 700; 155 | } 156 | 157 | a:visited, 158 | a:hover { 159 | color: #7EB6FF; 160 | } 161 | 162 | .g-recaptcha, .g-recaptcha > div { 163 | margin: auto; 164 | } 165 | -------------------------------------------------------------------------------- /public/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outsideris/slack-invite-automation/95de5051925c1ecc4dae5808bdf3a22b3a428357/public/images/bg.jpg -------------------------------------------------------------------------------- /public/images/field.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outsideris/slack-invite-automation/95de5051925c1ecc4dae5808bdf3a22b3a428357/public/images/field.png -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const request = require('request'); 4 | 5 | const config = require('../config'); 6 | const { badge } = require('../lib/badge'); 7 | 8 | const sanitize = require('sanitize'); 9 | 10 | router.get('/', function(req, res) { 11 | res.setLocale(config.locale); 12 | res.render('index', { community: config.community, 13 | tokenRequired: !!config.inviteToken, 14 | recaptchaSiteKey: config.recaptchaSiteKey }); 15 | }); 16 | 17 | router.post('/invite', function(req, res) { 18 | if (req.body.email && (!config.inviteToken || (!!config.inviteToken && req.body.token === config.inviteToken))) { 19 | function doInvite() { 20 | request.post({ 21 | url: 'https://'+ config.slackUrl + '/api/users.admin.invite', 22 | form: { 23 | email: req.body.email, 24 | token: config.slacktoken, 25 | set_active: true 26 | } 27 | }, function(err, httpResponse, body) { 28 | // body looks like: 29 | // {"ok":true} 30 | // or 31 | // {"ok":false,"error":"already_invited"} 32 | if (err) { return res.send('Error:' + err); } 33 | body = JSON.parse(body); 34 | if (body.ok) { 35 | res.render('result', { 36 | community: config.community, 37 | message: 'Success! Check “'+ req.body.email +'” for an invite from Slack.' 38 | }); 39 | } else { 40 | let error = body.error; 41 | if (error === 'already_invited' || error === 'already_in_team') { 42 | res.render('result', { 43 | community: config.community, 44 | message: 'Success! You were already invited.
' + 45 | 'Visit '+ config.community +'' 46 | }); 47 | return; 48 | } else if (error === 'invalid_email') { 49 | error = 'The email you entered is an invalid email.'; 50 | } else if (error === 'invalid_auth') { 51 | error = 'Something has gone wrong. Please contact a system administrator.'; 52 | } 53 | 54 | res.render('result', { 55 | community: config.community, 56 | message: 'Failed! ' + error, 57 | isFailed: true 58 | }); 59 | } 60 | }); 61 | } 62 | if (!!config.recaptchaSiteKey && !!config.recaptchaSecretKey) { 63 | request.post({ 64 | url: 'https://www.google.com/recaptcha/api/siteverify', 65 | form: { 66 | response: req.body['g-recaptcha-response'], 67 | secret: config.recaptchaSecretKey 68 | } 69 | }, function(err, httpResponse, body) { 70 | if (typeof body === "string") { 71 | body = JSON.parse(body); 72 | } 73 | 74 | if (body.success) { 75 | doInvite(); 76 | } else { 77 | error = 'Invalid captcha.'; 78 | res.render('result', { 79 | community: config.community, 80 | message: 'Failed! ' + error, 81 | isFailed: true 82 | }); 83 | } 84 | }); 85 | } else { 86 | doInvite(); 87 | } 88 | } else { 89 | const errMsg = []; 90 | if (!req.body.email) { 91 | errMsg.push('your email is required'); 92 | } 93 | 94 | if (!!config.inviteToken) { 95 | if (!req.body.token) { 96 | errMsg.push('valid token is required'); 97 | } 98 | 99 | if (req.body.token && req.body.token !== config.inviteToken) { 100 | errMsg.push('the token you entered is wrong'); 101 | } 102 | } 103 | 104 | res.render('result', { 105 | community: config.community, 106 | message: 'Failed! ' + errMsg.join(' and ') + '.', 107 | isFailed: true 108 | }); 109 | } 110 | }); 111 | 112 | router.get('/badge.svg', (req, res) => { 113 | request.get({ 114 | url: 'https://'+ config.slackUrl + '/api/users.list', 115 | qs: { 116 | token: config.slacktoken, 117 | presence: true 118 | } 119 | }, function(err, httpResponse, body) { 120 | try { 121 | body = JSON.parse(body); 122 | } catch(e) { 123 | return res.status(404).send(''); 124 | } 125 | if (!body.members) { 126 | return res.status(404).send(''); 127 | } 128 | 129 | const members = body.members.filter(function(m) { 130 | return !m.is_bot; 131 | }); 132 | const total = members.length; 133 | const presence = members.filter(function(m) { 134 | return m.presence === 'active'; 135 | }).length; 136 | 137 | const hexColor = /^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; 138 | sanitize.middleware.mixinFilters(req); 139 | 140 | res.type('svg'); 141 | res.set('Cache-Control', 'max-age=0, no-cache'); 142 | res.set('Pragma', 'no-cache'); 143 | res.send( 144 | badge( 145 | presence, 146 | total, 147 | req.queryPattern('colorA', hexColor), 148 | req.queryPattern('colorB', hexColor) 149 | ) 150 | ); 151 | }); 152 | }); 153 | 154 | module.exports = router; 155 | -------------------------------------------------------------------------------- /screenshots/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outsideris/slack-invite-automation/95de5051925c1ecc4dae5808bdf3a22b3a428357/screenshots/badge.png -------------------------------------------------------------------------------- /screenshots/basic_info-client_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outsideris/slack-invite-automation/95de5051925c1ecc4dae5808bdf3a22b3a428357/screenshots/basic_info-client_id.png -------------------------------------------------------------------------------- /screenshots/join-page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outsideris/slack-invite-automation/95de5051925c1ecc4dae5808bdf3a22b3a428357/screenshots/join-page.jpg -------------------------------------------------------------------------------- /screenshots/legacy-token.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outsideris/slack-invite-automation/95de5051925c1ecc4dae5808bdf3a22b3a428357/screenshots/legacy-token.gif -------------------------------------------------------------------------------- /screenshots/oauth1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outsideris/slack-invite-automation/95de5051925c1ecc4dae5808bdf3a22b3a428357/screenshots/oauth1.gif -------------------------------------------------------------------------------- /screenshots/oauth2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outsideris/slack-invite-automation/95de5051925c1ecc4dae5808bdf3a22b3a428357/screenshots/oauth2.gif -------------------------------------------------------------------------------- /screenshots/oauth3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outsideris/slack-invite-automation/95de5051925c1ecc4dae5808bdf3a22b3a428357/screenshots/oauth3.gif -------------------------------------------------------------------------------- /screenshots/oauth4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outsideris/slack-invite-automation/95de5051925c1ecc4dae5808bdf3a22b3a428357/screenshots/oauth4.gif -------------------------------------------------------------------------------- /screenshots/recaptcha.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/outsideris/slack-invite-automation/95de5051925c1ecc4dae5808bdf3a22b3a428357/screenshots/recaptcha.gif -------------------------------------------------------------------------------- /views/error.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(http-equiv="Content-Type", content="text/html; charset=utf-8") 5 | meta(name="viewport", content="width=device-width, initial-scale=1") 6 | title #{__('TITLE', community)} 7 | link(href="css/style.css", rel="stylesheet", type="text/css") 8 | link(href="//fonts.googleapis.com/css?family=Lato:300,400,700,900,700italic|Open+Sans:700italic,400,600,300,700,800", rel="stylesheet", type="text/css") 9 | body 10 | #wrapper 11 | .main 12 | .header 13 | h1= message 14 | h2= error.status 15 | pre #{error.stack} 16 | -------------------------------------------------------------------------------- /views/index.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(http-equiv="Content-Type", content="text/html; charset=utf-8") 5 | meta(name="viewport", content="width=device-width, initial-scale=1") 6 | title #{__('TITLE', community)} 7 | link(href="css/style.css", rel="stylesheet", type="text/css") 8 | link(href="//fonts.googleapis.com/css?family=Lato:300,400,700,900,700italic|Open+Sans:700italic,400,600,300,700,800", rel="stylesheet", type="text/css") 9 | body 10 | #wrapper 11 | .main 12 | .header 13 | h1 14 | strong #{community} 15 | h2 #{__('HEADER', community)} 16 | .content 17 | .information 18 | form(method="POST", action="invite")#join-form.form 19 | input(type="email", name="email", autofocus, placeholder=__('ENTER_EMAIL'))#slack-email.field 20 | if tokenRequired 21 | input(type="text", name="token", placeholder=__('ENTER_TOKEN'))#slack-token.field 22 | input(type="submit", value="Join").submit 23 | if !!recaptchaSiteKey 24 | div(class="g-recaptcha", data-sitekey=recaptchaSiteKey) 25 | if !!recaptchaSiteKey 26 | script(src='https://www.google.com/recaptcha/api.js') 27 | script. 28 | var tokenRequired = #{tokenRequired}; 29 | var form = document.getElementById('join-form'); 30 | var email = document.getElementById('slack-email'); 31 | var token = document.getElementById('slack-token'); 32 | form.addEventListener('submit', function(evt) { 33 | if (!email.value) { 34 | evt.preventDefault(); 35 | } 36 | if (tokenRequired && !token.value) { 37 | evt.preventDefault(); 38 | } 39 | var recaptcha = document.getElementById('g-recaptcha-response'); 40 | if (recaptcha && recaptcha.value === '') { 41 | evt.preventDefault(); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /views/result.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(http-equiv="Content-Type", content="text/html; charset=utf-8") 5 | meta(name="viewport", content="width=device-width, initial-scale=1") 6 | title #{__('TITLE', community)} 7 | link(href="css/style.css", rel="stylesheet", type="text/css") 8 | link(href="//fonts.googleapis.com/css?family=Lato:300,400,700,900,700italic|Open+Sans:700italic,400,600,300,700,800", rel="stylesheet", type="text/css") 9 | body 10 | #wrapper 11 | .main 12 | .header 13 | h1 14 | strong #{community} 15 | h2(class=isFailed?'error':'') !{message} 16 | --------------------------------------------------------------------------------