diff --git a/.gitignore b/.gitignore index 2e69312..ae9c7a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -# build output -dist + # dependencies node_modules diff --git a/Dockerfile b/Dockerfile index 9bd894b..38b699c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,4 +10,4 @@ RUN npm install --unsafe-perm EXPOSE 3000 -CMD ./bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN +CMD ./bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN $GOOGLE_CAPTCHA_SECRET $GOOGLE_CAPTCHA_SITEKEY diff --git a/Procfile b/Procfile index 4ef841b..618b7fb 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN +web: bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN $GOOGLE_CAPTCHA_SECRET $GOOGLE_CAPTCHA_SITEKEY diff --git a/app.json b/app.json index f82aedd..4059674 100644 --- a/app.json +++ b/app.json @@ -13,6 +13,14 @@ "description": "A Slack API token (find it on https://api.slack.com/web)", "required": true }, + "GOOGLE_CAPTCHA_SECRET": { + "description": "Google captcha secret key", + "required": true + }, + "GOOGLE_CAPTCHA_SITEKEY": { + "description": "Google captcha site key", + "required": true + }, "SLACK_COC": { "description": "A URL to a Code of Conduct people must agree on before joining.", "required": false diff --git a/bin/slackin b/bin/slackin index e09d532..8e623f9 100755 --- a/bin/slackin +++ b/bin/slackin @@ -17,28 +17,35 @@ args .option(['?', 'help'], 'Show the usage information') var flags = args.parse(process.argv, { - value: ' ', + value: ' ', help: false }) var org = args.sub[0] || process.env.SLACK_SUBDOMAIN var token = args.sub[1] || process.env.SLACK_API_TOKEN +var gcaptcha_secret = args.sub[2] || process.env.SLACK_SUBDOMAIN +var gcaptcha_sitekey = args.sub[3] || process.env.SLACK_API_TOKEN + + if (flags.help) { args.showHelp() } -if (!org || !token) { +if (!org || !token || !gcaptcha_sitekey || !gcaptcha_secret) { args.showHelp() } else { flags.org = org flags.token = token + flags.gcaptcha_secret = gcaptcha_secret + flags.gcaptcha_sitekey = gcaptcha_sitekey } var port = flags.port var hostname = flags.hostname slackin(flags).listen(port, hostname, function (err) { + console.log("FLAGS: ", flags); if (err) throw err if (!flags.silent) console.log('%s – listening on %s:%d', new Date, hostname, port) }) diff --git a/dist/assets/badge.js b/dist/assets/badge.js new file mode 100644 index 0000000..924b8ca --- /dev/null +++ b/dist/assets/badge.js @@ -0,0 +1,250 @@ +(function (){ + + // give up and resort to `target=_blank` + // if we're not modern enough + if (!document.body.getBoundingClientRect + || !document.body.querySelectorAll + || !window.postMessage) { + return + } + + // search for a script tag pointing to slackin.js + function search (){ + var replaced = 0 + var scripts = document.querySelectorAll('script') + var script + for (var i = 0; i < scripts.length; i++) { + script = scripts[i] + if (!script.src) continue + if (/\/slackin\.js(\?.*)?$/.test(script.src)) { + // replace script with iframe + replace(script) + + // we abort the search for subsequent + // slackin.js executions to exhaust + // the queue + return true + } + } + } + + var LARGE // boolean for large/small mode, from query param + + // replace the script tag with an iframe + function replace (script){ + var parent = script.parentNode + if (!parent) return + + LARGE = /\?large/.test(script.src) + var iframe = document.createElement('iframe') + var iframePath = '/iframe' + (LARGE ? '?large' : '') + iframe.src = script.src.replace(/\/slackin\.js.*/, iframePath) + iframe.style.borderWidth = 0 + iframe.className = '__slackin' + + // a decent aproximation that we adjust later + // once we have the knowledge of the actual + // numbers of users, based on a user count + // of 3 digits by 3 digits + iframe.style.width = (LARGE ? 190 : 140) + 'px' + + // height depends on target size + iframe.style.height = (LARGE ? 30 : 20) + 'px' + + // hidden by default to avoid flicker + iframe.style.visibility = 'hidden' + + parent.insertBefore(iframe, script) + parent.removeChild(script) + + // setup iframe RPC + iframe.onload = function (){ + setup(iframe) + } + } + + // setup an "RPC" channel between iframe and us + function setup (iframe){ + var id = Math.random() * (1 << 24) | 0 + iframe.contentWindow.postMessage('slackin:' + id, '*') + window.addEventListener('message', function (e){ + if (typeof e.data !== 'string') return + + // show dialog upon click + if ('slackin-click:' + id === e.data) { + showDialog(iframe) + } + + // update width + var wp = 'slackin-width:' + id + ':' + if (wp === e.data.substr(0, wp.length)) { + var width = e.data.substr(wp.length) + iframe.style.width = width + 'px' + + // ensure it's shown (since first time hidden) + iframe.style.visibility = 'visible' + } + + // redirect to URL + var redir = 'slackin-redirect:' + id + ':' + if (redir === e.data.substr(0, redir.length)) { + location.href = e.data.substr(redir.length) + } + }) + } + + // show the dialog around the iframe + // by, yes, creating a new iframe + var showing = false + function showDialog (iframe){ + if (showing) return + showing = true + + if (LARGE) { + unitSize = '14px' + arrowHeight = 13 + } else { + unitSize = '10px' + arrowHeight = 9 + } + + // container div + var div = document.createElement('div') + div.className = '__slackin' + div.style.fontSize = unitSize + div.style.border = '.1em solid #D6D6D6' + div.style.padding = '0' + div.style.margin = '0' + div.style.lineHeight = '0' + div.style.backgroundColor = '#FAFAFA' + div.style.width = '25em' + div.style.height = '15.5em' + div.style.position = 'absolute' + div.style.left = '-10000px' + div.style.top = '-10000px' + div.style.borderRadius = '.4em' + div.style.padding = '.4em' + div.style.boxSizing = 'content-box' + + // new iframe + var ni = document.createElement('iframe') + ni.className = '__slackin' + ni.style.width = '25em' + ni.style.height = '15.5em' + ni.style.borderWidth = 0 + ni.src = iframe.src.replace('iframe', 'iframe/dialog') + ni.onload = function (){ + setup(ni) + window.addEventListener('scroll', dposition) + window.addEventListener('resize', dposition) + position() + } + + // arrows + var a1 = document.createElement('div') + var a2 = document.createElement('div'); + [a1, a2].forEach(function (a){ + a.style.border = 'solid transparent' + a.style.pointerEvents = 'none' + a.style.width = '0' + a.style.height = '0' + a.style.margin = '0' + a.style.padding = '0' + a.style.position = 'absolute' + a.style.display = 'inline' + }) + + a1.style.borderColor = 'rgba(214, 214, 214, 0)' + a2.style.borderColor = 'rgba(250, 250, 250, 0)' + + a1.style.borderWidth = '.7em' + a1.style.marginLeft = '-.65em' + a2.style.borderWidth = '.6em' + a2.style.marginLeft = '-.6em' + + // append + div.appendChild(a1) + div.appendChild(a2) + div.appendChild(ni) + document.body.appendChild(div) + + function position (){ + [div, a1, a2].forEach(function (el){ + el.style.left = '' + el.style.right = '' + el.style.bottom = '' + el.style.top = '' + }) + + var divPos = div.getBoundingClientRect() + var iframePos = iframe.getBoundingClientRect() + var divHeight = divPos.height + arrowHeight + + var st = document.body.scrollTop + var sl = document.body.scrollLeft + var iw = window.innerWidth + var ih = window.innerHeight + var iframeTop = iframePos.top + st + var iframeLeft = iframePos.left + sl + + // position vertically / arrows + if (st + iframePos.bottom + divHeight > st + ih) { + div.style.top = (iframeTop - divHeight) + 'px' + a1.style.top = a2.style.top = '100%' + + a1.style.borderBottomColor = 'rgba(214, 214, 214, 0)' + a2.style.borderBottomColor = 'rgba(250, 250, 250, 0)' + a1.style.borderTopColor = '#d6d6d6' + a2.style.borderTopColor = '#fafafa' + } else { + div.style.top = (iframeTop + iframePos.height + arrowHeight) + 'px' + a1.style.bottom = a2.style.bottom = '100%' + + a1.style.borderTopColor = 'rgba(214, 214, 214, 0)' + a2.style.borderTopColor = 'rgba(250, 250, 250, 0)' + a1.style.borderBottomColor = '#d6d6d6' + a2.style.borderBottomColor = '#fafafa' + } + + // position horizontally + var left = iframePos.left + + Math.round(iframePos.width / 2) + - Math.round(divPos.width / 2) + if (left < sl) left = sl + if (left + divPos.width > sl + iw) { + left = sl + iw - divPos.width + } + div.style.left = left + 'px' + + a1.style.left = + a2.style.left = (iframeLeft - left + Math.round(iframePos.width / 2)) + 'px' + } + + // debounced positionining + var timer + function dposition (){ + clearTimeout(timer) + timer = setTimeout(position, 100) + } + + function hide (){ + showing = false + window.removeEventListener('scroll', dposition) + window.removeEventListener('resize', dposition) + document.body.removeChild(div) + document.documentElement.removeEventListener('click', click, true) + } + + function click (ev){ + if ('__slackin' != ev.target.className) { + hide() + } + } + + document.documentElement.addEventListener('click', click, true) + } + + var found = search() + if (!found) setTimeout(search, 5000) + +})() diff --git a/dist/assets/checkbox-checked.svg b/dist/assets/checkbox-checked.svg new file mode 100644 index 0000000..b63bb25 --- /dev/null +++ b/dist/assets/checkbox-checked.svg @@ -0,0 +1 @@ +Untitled \ No newline at end of file diff --git a/dist/assets/checkbox.png b/dist/assets/checkbox.png new file mode 100644 index 0000000..1387730 Binary files /dev/null and b/dist/assets/checkbox.png differ diff --git a/dist/assets/checkbox.svg b/dist/assets/checkbox.svg new file mode 100644 index 0000000..1aef2ba --- /dev/null +++ b/dist/assets/checkbox.svg @@ -0,0 +1 @@ +Untitled \ No newline at end of file diff --git a/dist/assets/client.js b/dist/assets/client.js new file mode 100644 index 0000000..fe7f14b --- /dev/null +++ b/dist/assets/client.js @@ -0,0 +1,104 @@ +/* global io,superagent */ + +var body = document.body +var request = superagent + +// elements +var form = body.querySelector('form#invite') +var channel = form.elements['channel'] || {} +var email = form.elements['email'] +var coc = form.elements['coc'] +var button = body.querySelector('button') + +// remove loading state +button.className = '' + +// capture submit +body.addEventListener('submit', function (ev){ + ev.preventDefault() + button.disabled = true + button.className = '' + button.innerHTML = 'Please Wait' + invite(channel ? channel.value : null, coc && coc.checked ? 1 : 0, email.value, function (err, msg){ + if (err) { + button.removeAttribute('disabled') + button.className = 'error' + button.innerHTML = err.message + } else { + button.className = 'success' + button.innerHTML = msg + } + }) +}) + +function invite (channel, coc, email, fn){ + request + .post(data.path + 'invite') + .send({ + coc: coc, + channel: channel, + email: email + }) + .end(function (res){ + if (res.body.redirectUrl) { + var err = new Error(res.body.msg || 'Server error') + window.setTimeout(function () { + topLevelRedirect(res.body.redirectUrl) + }, 1500) + } + if (res.error) { + var err = new Error(res.body.msg || 'Server error') + return fn(err) + } else { + fn(null, res.body.msg) + } + }) +} + +// use dom element for better cross browser compatibility +var url = document.createElement('a') +url.href = window.location +// realtime updates +var socket = io({ path: data.path + 'socket.io' }) +socket.on('data', function (users){ + for (var i in users) update(i, users[i]) +}) +socket.on('total', function (n){ update('total', n) }) +socket.on('active', function (n){ update('active', n) }) + +function update (val, n, noanim){ + var el = document.querySelector('.' + val) + if (n != el.innerHTML) { + el.innerHTML = n + anim(el, val) + } +} + +function anim (el, c){ + if (el.anim) return + el.className = c + ' grow' + el.anim = setTimeout(function (){ + el.className = c + el.anim = null + }, 150) +} + +// redirect, using "RPC" to parent if necessary +function topLevelRedirect (url) { + if (window === top) location.href = url + else parent.postMessage('slackin-redirect:' + id + ':' + url, '*') + // Q: Why can't we just `top.location.href = url;`? + // A: + // [sandboxing]: http://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/ + // [CSP]: http://www.html5rocks.com/en/tutorials/security/content-security-policy/ + // [nope]: http://output.jsbin.com/popawuk/16 +}; + +// "RPC" channel to parent +var id +window.addEventListener('message', function onmsg (e){ + if (/^slackin:/.test(e.data)) { + id = e.data.replace(/^slackin:/, '') + window.removeEventListener('message', onmsg) + } +}) diff --git a/dist/assets/iframe-button.css b/dist/assets/iframe-button.css new file mode 100644 index 0000000..49cf5e1 --- /dev/null +++ b/dist/assets/iframe-button.css @@ -0,0 +1,139 @@ + +/* + CSS from: https://github.com/mdo/github-buttons + Copyright 2014-2015 Mark Otto. Released under Apache 2.0. +*/ + +body, html { + background: transparent; +} + +body { + padding: 0; + margin: 0; + font: bold 11px/14px "Helvetica Neue", Helvetica, Arial, sans-serif; + overflow: hidden; +} +.slack-button { + height: 20px; + overflow: hidden; + display: inline-block; +} +.slack-btn, +.slack-count, +.slack-ico { + float: left; +} +.slack-btn, +.slack-count { + padding: 2px 5px 2px 4px; + color: #333; + text-decoration: none; + text-shadow: 0 1px 0 #fff; + white-space: nowrap; + cursor: pointer; + border-radius: 3px; + transition: background-color 200ms ease-in; +} +.slack-btn { + background-color: #eee; + background-image: -moz-linear-gradient(#fcfcfc, #eee); + background-image: -webkit-linear-gradient(#fcfcfc, #eee); + background-image: linear-gradient(#fcfcfc, #eee); + background-repeat: no-repeat; + border: 1px solid #d5d5d5; +} +.slack-btn:hover, +.slack-btn:focus { + text-decoration: none; + background-color: #ddd; + background-image: -moz-linear-gradient(#eee, #ddd); + background-image: -webkit-linear-gradient(#eee, #ddd); + background-image: linear-gradient(#eee, #ddd); + border-color: #ccc; +} +.slack-btn:active { + background-image: none; + background-color: #dcdcdc; + border-color: #b5b5b5; + box-shadow: inset 0 2px 4px rgba(0,0,0,0.15); +} +.slack-ico { + width: 14px; + height: 14px; + margin-right: 4px; + background-size: 100% 100%; + background-repeat: no-repeat; +} +.slack-count { + position: relative; + display: block; + margin-left: 4px; + background-color: #fafafa; + border: 1px solid #d4d4d4; +} +.slack-count:hover, +.slack-count:focus { + color: #4183C4; +} +.slack-count:before, +.slack-count:after { + content: ''; + position: absolute; + display: inline-block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + transition: border-color 200ms ease-in; +} +.slack-count:before { + top: 50%; + left: -3px; + margin-top: -4px; + border-width: 4px 4px 4px 0; + border-right-color: #fafafa; +} +.slack-count:after { + top: 50%; + left: -4px; + z-index: -1; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #d4d4d4; +} + +.slack-count.anim { + background-color: yellow; +} +.slack-count.anim:before { + border-right-color: yellow; +} + +.slack-btn-large { + height: 30px; +} +.slack-btn-large .slack-btn, +.slack-btn-large .slack-count { + padding: 3px 10px 3px 8px; + font-size: 16px; + line-height: 22px; + border-radius: 4px; +} +.slack-btn-large .slack-ico { + width: 20px; + height: 20px; +} +.slack-btn-large .slack-count { + margin-left: 6px; +} +.slack-btn-large .slack-count:before { + left: -5px; + margin-top: -6px; + border-width: 6px 6px 6px 0; +} +.slack-btn-large .slack-count:after { + left: -6px; + margin-top: -7px; + border-width: 7px 7px 7px 0; +} diff --git a/dist/assets/iframe.js b/dist/assets/iframe.js new file mode 100644 index 0000000..d1d6ba4 --- /dev/null +++ b/dist/assets/iframe.js @@ -0,0 +1,88 @@ +/* global io,data */ + +(function (){ + + // give up and resort to `target=_blank` + // if we're not modern enough + if (!document.body.getBoundingClientRect + || !document.body.querySelectorAll + || !window.postMessage) { + return + } + + // the id for the script we capture + var id + + // listen on setup event from the parent + // to set up the id + window.addEventListener('message', function onmsg (e){ + if (/^slackin:/.test(e.data)) { + id = e.data.replace(/^slackin:/, '') + document.body.addEventListener('click', function (ev){ + var el = ev.target + while (el && 'A' != el.nodeName) el = el.parentNode + if (el && '_blank' === el.target) { + ev.preventDefault() + parent.postMessage('slackin-click:' + id, '*') + } + }) + window.removeEventListener('message', onmsg) + + // notify initial width + refresh() + } + }) + + // notify parent about current width + var button = document.querySelector('.slack-button') + var lastWidth + function refresh (){ + var width = button.getBoundingClientRect().width + if (top != window && window.postMessage) { + var but = document.querySelector('.slack-button') + var width = Math.ceil(but.getBoundingClientRect().width) + if (lastWidth != width) { + lastWidth = width + parent.postMessage('slackin-width:' + id + ':' + width, '*') + } + } + } + + // initialize realtime events asynchronously + var script = document.createElement('script') + script.src = 'https://cdn.socket.io/socket.io-1.4.4.js' + script.onload = function (){ + + // use dom element for better cross browser compatibility + var url = document.createElement('a') + url.href = window.location + var socket = io({ path: data.path + 'socket.io' }) + var count = document.getElementsByClassName('slack-count')[0] + + socket.on('data', function (users){ + for (var i in users) update(i, users[i]) + }) + socket.on('total', function (n){ update('total', n) }) + socket.on('active', function (n){ update('active', n) }) + + var anim + function update (key, n) { + if (n != data[key]) { + data[key] = n + var str = '' + if (data.active) str = data.active + '/' + if (data.total) str += data.total + if (!str.length) str = '–' + if (anim) clearTimeout(anim) + count.innerHTML = str + count.className = 'slack-count anim' + anim = setTimeout(function (){ + count.className = 'slack-count' + }, 200) + refresh() + } + } + } + document.body.appendChild(script) + +})() diff --git a/dist/assets/slack.svg b/dist/assets/slack.svg new file mode 100644 index 0000000..116e85a --- /dev/null +++ b/dist/assets/slack.svg @@ -0,0 +1 @@ +Group \ No newline at end of file diff --git a/dist/assets/superagent.js b/dist/assets/superagent.js new file mode 100644 index 0000000..39ea211 --- /dev/null +++ b/dist/assets/superagent.js @@ -0,0 +1,1493 @@ +;(function (){ + +/** + * Require the given path. + * + * @param {String} path + * @return {Object} exports + * @api public + */ + + function require (path, parent, orig) { + var resolved = require.resolve(path) + + // lookup failed + if (null == resolved) { + orig = orig || path + parent = parent || 'root' + var err = new Error('Failed to require "' + orig + '" from "' + parent + '"') + err.path = orig + err.parent = parent + err.require = true + throw err + } + + var module = require.modules[resolved] + + // perform real require() + // by invoking the module's + // registered function + if (!module._resolving && !module.exports) { + var mod = {} + mod.exports = {} + mod.client = mod.component = true + module._resolving = true + module.call(this, mod.exports, require.relative(resolved), mod) + delete module._resolving + module.exports = mod.exports + } + + return module.exports + } + +/** + * Registered modules. + */ + + require.modules = {} + +/** + * Registered aliases. + */ + + require.aliases = {} + +/** + * Resolve `path`. + * + * Lookup: + * + * - PATH/index.js + * - PATH.js + * - PATH + * + * @param {String} path + * @return {String} path or null + * @api private + */ + + require.resolve = function (path) { + if (path.charAt(0) === '/') path = path.slice(1) + + var paths = [ + path, + path + '.js', + path + '.json', + path + '/index.js', + path + '/index.json' + ] + + for (var i = 0; i < paths.length; i++) { + var path = paths[i] + if (require.modules.hasOwnProperty(path)) return path + if (require.aliases.hasOwnProperty(path)) return require.aliases[path] + } + } + +/** + * Normalize `path` relative to the current path. + * + * @param {String} curr + * @param {String} path + * @return {String} + * @api private + */ + + require.normalize = function (curr, path) { + var segs = [] + + if ('.' != path.charAt(0)) return path + + curr = curr.split('/') + path = path.split('/') + + for (var i = 0; i < path.length; ++i) { + if ('..' === path[i]) { + curr.pop() + } else if ('.' != path[i] && '' != path[i]) { + segs.push(path[i]) + } + } + + return curr.concat(segs).join('/') + } + +/** + * Register module at `path` with callback `definition`. + * + * @param {String} path + * @param {Function} definition + * @api private + */ + + require.register = function (path, definition) { + require.modules[path] = definition + } + +/** + * Alias a module definition. + * + * @param {String} from + * @param {String} to + * @api private + */ + + require.alias = function (from, to) { + if (!require.modules.hasOwnProperty(from)) { + throw new Error('Failed to alias "' + from + '", it does not exist') + } + require.aliases[to] = from + } + +/** + * Return a require function relative to the `parent` path. + * + * @param {String} parent + * @return {Function} + * @api private + */ + + require.relative = function (parent) { + var p = require.normalize(parent, '..') + + /** + * lastIndexOf helper. + */ + + function lastIndexOf (arr, obj) { + var i = arr.length + while (i--) { + if (arr[i] === obj) return i + } + return -1 + } + + /** + * The relative require() itself. + */ + + function localRequire (path) { + var resolved = localRequire.resolve(path) + return require(resolved, parent, path) + } + + /** + * Resolve relative to the parent. + */ + + localRequire.resolve = function (path) { + var c = path.charAt(0) + if ('/' === c) return path.slice(1) + if ('.' === c) return require.normalize(p, path) + + // resolve deps by returning + // the dep in the nearest "deps" + // directory + var segs = parent.split('/') + var i = lastIndexOf(segs, 'deps') + 1 + if (!i) i = 0 + path = segs.slice(0, i + 1).join('/') + '/deps/' + path + return path + } + + /** + * Check if module is defined at `path`. + */ + + localRequire.exists = function (path) { + return require.modules.hasOwnProperty(localRequire.resolve(path)) + } + + return localRequire + } + require.register('component-emitter/index.js', function (exports, require, module){ + +/** + * Expose `Emitter`. + */ + + module.exports = Emitter + +/** + * Initialize a new `Emitter`. + * + * @api public + */ + + function Emitter (obj) { + if (obj) return mixin(obj) + }; + +/** + * Mixin the emitter properties. + * + * @param {Object} obj + * @return {Object} + * @api private + */ + + function mixin (obj) { + for (var key in Emitter.prototype) { + obj[key] = Emitter.prototype[key] + } + return obj + } + +/** + * Listen on the given `event` with `fn`. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + + Emitter.prototype.on = +Emitter.prototype.addEventListener = function (event, fn){ + this._callbacks = this._callbacks || {}; + (this._callbacks[event] = this._callbacks[event] || []) + .push(fn) + return this +} + +/** + * Adds an `event` listener that will be invoked a single + * time then automatically removed. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + + Emitter.prototype.once = function (event, fn){ + var self = this + this._callbacks = this._callbacks || {} + + function on () { + self.off(event, on) + fn.apply(this, arguments) + } + + on.fn = fn + this.on(event, on) + return this + } + +/** + * Remove the given callback for `event` or all + * registered callbacks. + * + * @param {String} event + * @param {Function} fn + * @return {Emitter} + * @api public + */ + + Emitter.prototype.off = +Emitter.prototype.removeListener = +Emitter.prototype.removeAllListeners = +Emitter.prototype.removeEventListener = function (event, fn){ + this._callbacks = this._callbacks || {} + + // all + if (0 === arguments.length) { + this._callbacks = {} + return this + } + + // specific event + var callbacks = this._callbacks[event] + if (!callbacks) return this + + // remove all handlers + if (1 === arguments.length) { + delete this._callbacks[event] + return this + } + + // remove specific handler + var cb + for (var i = 0; i < callbacks.length; i++) { + cb = callbacks[i] + if (cb === fn || cb.fn === fn) { + callbacks.splice(i, 1) + break + } + } + return this +} + +/** + * Emit `event` with the given args. + * + * @param {String} event + * @param {Mixed} ... + * @return {Emitter} + */ + + Emitter.prototype.emit = function (event){ + this._callbacks = this._callbacks || {} + var args = [].slice.call(arguments, 1), + callbacks = this._callbacks[event] + + if (callbacks) { + callbacks = callbacks.slice(0) + for (var i = 0, len = callbacks.length; i < len; ++i) { + callbacks[i].apply(this, args) + } + } + + return this + } + +/** + * Return array of callbacks for `event`. + * + * @param {String} event + * @return {Array} + * @api public + */ + + Emitter.prototype.listeners = function (event){ + this._callbacks = this._callbacks || {} + return this._callbacks[event] || [] + } + +/** + * Check if this emitter has `event` handlers. + * + * @param {String} event + * @return {Boolean} + * @api public + */ + + Emitter.prototype.hasListeners = function (event){ + return !! this.listeners(event).length + } + + }) + require.register('component-reduce/index.js', function (exports, require, module){ + +/** + * Reduce `arr` with `fn`. + * + * @param {Array} arr + * @param {Function} fn + * @param {Mixed} initial + * + * TODO: combatible error handling? + */ + + module.exports = function (arr, fn, initial){ + var idx = 0 + var len = arr.length + var curr = arguments.length === 3 + ? initial + : arr[idx++] + + while (idx < len) { + curr = fn.call(null, curr, arr[idx], ++idx, arr) + } + + return curr + } + }) + require.register('superagent/lib/client.js', function (exports, require, module){ +/** + * Module dependencies. + */ + + var Emitter = require('emitter') + var reduce = require('reduce') + +/** + * Root reference for iframes. + */ + + var root = 'undefined' === typeof window + ? this + : window + +/** + * Noop. + */ + + function noop (){}; + +/** + * Check if `obj` is a host object, + * we don't want to serialize these :) + * + * TODO: future proof, move to compoent land + * + * @param {Object} obj + * @return {Boolean} + * @api private + */ + + function isHost (obj) { + var str = {}.toString.call(obj) + + switch (str) { + case '[object File]': + case '[object Blob]': + case '[object FormData]': + return true + default: + return false + } + } + +/** + * Determine XHR. + */ + + function getXHR () { + if (root.XMLHttpRequest + && ('file:' != root.location.protocol || !root.ActiveXObject)) { + return new XMLHttpRequest + } else { + try { return new ActiveXObject('Microsoft.XMLHTTP') } catch(e) {} + try { return new ActiveXObject('Msxml2.XMLHTTP.6.0') } catch(e) {} + try { return new ActiveXObject('Msxml2.XMLHTTP.3.0') } catch(e) {} + try { return new ActiveXObject('Msxml2.XMLHTTP') } catch(e) {} + } + return false + } + +/** + * Removes leading and trailing whitespace, added to support IE. + * + * @param {String} s + * @return {String} + * @api private + */ + + var trim = ''.trim + ? function (s) { return s.trim() } + : function (s) { return s.replace(/(^\s*|\s*$)/g, '') } + +/** + * Check if `obj` is an object. + * + * @param {Object} obj + * @return {Boolean} + * @api private + */ + + function isObject (obj) { + return obj === Object(obj) + } + +/** + * Serialize the given `obj`. + * + * @param {Object} obj + * @return {String} + * @api private + */ + + function serialize (obj) { + if (!isObject(obj)) return obj + var pairs = [] + for (var key in obj) { + if (null != obj[key]) { + pairs.push(encodeURIComponent(key) + + '=' + encodeURIComponent(obj[key])) + } + } + return pairs.join('&') + } + +/** + * Expose serialization method. + */ + + request.serializeObject = serialize + + /** + * Parse the given x-www-form-urlencoded `str`. + * + * @param {String} str + * @return {Object} + * @api private + */ + + function parseString (str) { + var obj = {} + var pairs = str.split('&') + var parts + var pair + + for (var i = 0, len = pairs.length; i < len; ++i) { + pair = pairs[i] + parts = pair.split('=') + obj[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]) + } + + return obj + } + +/** + * Expose parser. + */ + + request.parseString = parseString + +/** + * Default MIME type map. + * + * superagent.types.xml = 'application/xml'; + * + */ + + request.types = { + html: 'text/html', + json: 'application/json', + xml: 'application/xml', + urlencoded: 'application/x-www-form-urlencoded', + 'form': 'application/x-www-form-urlencoded', + 'form-data': 'application/x-www-form-urlencoded' + } + +/** + * Default serialization map. + * + * superagent.serialize['application/xml'] = function(obj){ + * return 'generated xml here'; + * }; + * + */ + + request.serialize = { + 'application/x-www-form-urlencoded': serialize, + 'application/json': JSON.stringify + } + + /** + * Default parsers. + * + * superagent.parse['application/xml'] = function(str){ + * return { object parsed from str }; + * }; + * + */ + + request.parse = { + 'application/x-www-form-urlencoded': parseString, + 'application/json': JSON.parse + } + +/** + * Parse the given header `str` into + * an object containing the mapped fields. + * + * @param {String} str + * @return {Object} + * @api private + */ + + function parseHeader (str) { + var lines = str.split(/\r?\n/) + var fields = {} + var index + var line + var field + var val + + lines.pop() // trailing CRLF + + for (var i = 0, len = lines.length; i < len; ++i) { + line = lines[i] + index = line.indexOf(':') + field = line.slice(0, index).toLowerCase() + val = trim(line.slice(index + 1)) + fields[field] = val + } + + return fields + } + +/** + * Return the mime type for the given `str`. + * + * @param {String} str + * @return {String} + * @api private + */ + + function type (str){ + return str.split(/ *; */).shift() + }; + +/** + * Return header field parameters. + * + * @param {String} str + * @return {Object} + * @api private + */ + + function params (str){ + return reduce(str.split(/ *; */), function (obj, str){ + var parts = str.split(/ *= */), + key = parts.shift(), + val = parts.shift() + + if (key && val) obj[key] = val + return obj + }, {}) + }; + +/** + * Initialize a new `Response` with the given `xhr`. + * + * - set flags (.ok, .error, etc) + * - parse header + * + * Examples: + * + * Aliasing `superagent` as `request` is nice: + * + * request = superagent; + * + * We can use the promise-like API, or pass callbacks: + * + * request.get('/').end(function(res){}); + * request.get('/', function(res){}); + * + * Sending data can be chained: + * + * request + * .post('/user') + * .send({ name: 'tj' }) + * .end(function(res){}); + * + * Or passed to `.send()`: + * + * request + * .post('/user') + * .send({ name: 'tj' }, function(res){}); + * + * Or passed to `.post()`: + * + * request + * .post('/user', { name: 'tj' }) + * .end(function(res){}); + * + * Or further reduced to a single call for simple cases: + * + * request + * .post('/user', { name: 'tj' }, function(res){}); + * + * @param {XMLHTTPRequest} xhr + * @param {Object} options + * @api private + */ + + function Response (req, options) { + options = options || {} + this.req = req + this.xhr = this.req.xhr + this.text = this.req.method !='HEAD' + ? this.xhr.responseText + : null + this.setStatusProperties(this.xhr.status) + this.header = this.headers = parseHeader(this.xhr.getAllResponseHeaders()) + // getAllResponseHeaders sometimes falsely returns "" for CORS requests, but + // getResponseHeader still works. so we get content-type even if getting + // other headers fails. + this.header['content-type'] = this.xhr.getResponseHeader('content-type') + this.setHeaderProperties(this.header) + this.body = this.req.method != 'HEAD' + ? this.parseBody(this.text) + : null + } + +/** + * Get case-insensitive `field` value. + * + * @param {String} field + * @return {String} + * @api public + */ + + Response.prototype.get = function (field){ + return this.header[field.toLowerCase()] + } + +/** + * Set header related properties: + * + * - `.type` the content type without params + * + * A response of "Content-Type: text/plain; charset=utf-8" + * will provide you with a `.type` of "text/plain". + * + * @param {Object} header + * @api private + */ + + Response.prototype.setHeaderProperties = function (header){ + // content-type + var ct = this.header['content-type'] || '' + this.type = type(ct) + + // params + var obj = params(ct) + for (var key in obj) this[key] = obj[key] + } + +/** + * Parse the given body `str`. + * + * Used for auto-parsing of bodies. Parsers + * are defined on the `superagent.parse` object. + * + * @param {String} str + * @return {Mixed} + * @api private + */ + + Response.prototype.parseBody = function (str){ + var parse = request.parse[this.type] + return parse && str && str.length + ? parse(str) + : null + } + +/** + * Set flags such as `.ok` based on `status`. + * + * For example a 2xx response will give you a `.ok` of __true__ + * whereas 5xx will be __false__ and `.error` will be __true__. The + * `.clientError` and `.serverError` are also available to be more + * specific, and `.statusType` is the class of error ranging from 1..5 + * sometimes useful for mapping respond colors etc. + * + * "sugar" properties are also defined for common cases. Currently providing: + * + * - .noContent + * - .badRequest + * - .unauthorized + * - .notAcceptable + * - .notFound + * + * @param {Number} status + * @api private + */ + + Response.prototype.setStatusProperties = function (status){ + var type = status / 100 | 0 + + // status / class + this.status = status + this.statusType = type + + // basics + this.info = 1 === type + this.ok = 2 === type + this.clientError = 4 === type + this.serverError = 5 === type + this.error = (4 === type || 5 === type) + ? this.toError() + : false + + // sugar + this.accepted = 202 === status + this.noContent = 204 === status || 1223 === status + this.badRequest = 400 === status + this.unauthorized = 401 === status + this.notAcceptable = 406 === status + this.notFound = 404 === status + this.forbidden = 403 === status + } + +/** + * Return an `Error` representative of this response. + * + * @return {Error} + * @api public + */ + + Response.prototype.toError = function (){ + var req = this.req + var method = req.method + var url = req.url + + var msg = 'cannot ' + method + ' ' + url + ' (' + this.status + ')' + var err = new Error(msg) + err.status = this.status + err.method = method + err.url = url + + return err + } + +/** + * Expose `Response`. + */ + + request.Response = Response + +/** + * Initialize a new `Request` with the given `method` and `url`. + * + * @param {String} method + * @param {String} url + * @api public + */ + + function Request (method, url) { + var self = this + Emitter.call(this) + this._query = this._query || [] + this.method = method + this.url = url + this.header = {} + this._header = {} + this.on('end', function (){ + var err = null + var res = null + + try { + res = new Response(self) + } catch(e) { + err = new Error('Parser is unable to parse the response') + err.parse = true + err.original = e + } + + self.callback(err, res) + }) + } + +/** + * Mixin `Emitter`. + */ + + Emitter(Request.prototype) + +/** + * Allow for extension + */ + + Request.prototype.use = function (fn) { + fn(this) + return this + } + +/** + * Set timeout to `ms`. + * + * @param {Number} ms + * @return {Request} for chaining + * @api public + */ + + Request.prototype.timeout = function (ms){ + this._timeout = ms + return this + } + +/** + * Clear previous timeout. + * + * @return {Request} for chaining + * @api public + */ + + Request.prototype.clearTimeout = function (){ + this._timeout = 0 + clearTimeout(this._timer) + return this + } + +/** + * Abort the request, and clear potential timeout. + * + * @return {Request} + * @api public + */ + + Request.prototype.abort = function (){ + if (this.aborted) return + this.aborted = true + this.xhr.abort() + this.clearTimeout() + this.emit('abort') + return this + } + +/** + * Set header `field` to `val`, or multiple fields with one object. + * + * Examples: + * + * req.get('/') + * .set('Accept', 'application/json') + * .set('X-API-Key', 'foobar') + * .end(callback); + * + * req.get('/') + * .set({ Accept: 'application/json', 'X-API-Key': 'foobar' }) + * .end(callback); + * + * @param {String|Object} field + * @param {String} val + * @return {Request} for chaining + * @api public + */ + + Request.prototype.set = function (field, val){ + if (isObject(field)) { + for (var key in field) { + this.set(key, field[key]) + } + return this + } + this._header[field.toLowerCase()] = val + this.header[field] = val + return this + } + +/** + * Remove header `field`. + * + * Example: + * + * req.get('/') + * .unset('User-Agent') + * .end(callback); + * + * @param {String} field + * @return {Request} for chaining + * @api public + */ + + Request.prototype.unset = function (field){ + delete this._header[field.toLowerCase()] + delete this.header[field] + return this + } + +/** + * Get case-insensitive header `field` value. + * + * @param {String} field + * @return {String} + * @api private + */ + + Request.prototype.getHeader = function (field){ + return this._header[field.toLowerCase()] + } + +/** + * Set Content-Type to `type`, mapping values from `request.types`. + * + * Examples: + * + * superagent.types.xml = 'application/xml'; + * + * request.post('/') + * .type('xml') + * .send(xmlstring) + * .end(callback); + * + * request.post('/') + * .type('application/xml') + * .send(xmlstring) + * .end(callback); + * + * @param {String} type + * @return {Request} for chaining + * @api public + */ + + Request.prototype.type = function (type){ + this.set('Content-Type', request.types[type] || type) + return this + } + +/** + * Set Accept to `type`, mapping values from `request.types`. + * + * Examples: + * + * superagent.types.json = 'application/json'; + * + * request.get('/agent') + * .accept('json') + * .end(callback); + * + * request.get('/agent') + * .accept('application/json') + * .end(callback); + * + * @param {String} accept + * @return {Request} for chaining + * @api public + */ + + Request.prototype.accept = function (type){ + this.set('Accept', request.types[type] || type) + return this + } + +/** + * Set Authorization field value with `user` and `pass`. + * + * @param {String} user + * @param {String} pass + * @return {Request} for chaining + * @api public + */ + + Request.prototype.auth = function (user, pass){ + var str = btoa(user + ':' + pass) + this.set('Authorization', 'Basic ' + str) + return this + } + +/** +* Add query-string `val`. +* +* Examples: +* +* request.get('/shoes') +* .query('size=10') +* .query({ color: 'blue' }) +* +* @param {Object|String} val +* @return {Request} for chaining +* @api public +*/ + + Request.prototype.query = function (val){ + if ('string' != typeof val) val = serialize(val) + if (val) this._query.push(val) + return this + } + +/** + * Write the field `name` and `val` for "multipart/form-data" + * request bodies. + * + * ``` js + * request.post('/upload') + * .field('foo', 'bar') + * .end(callback); + * ``` + * + * @param {String} name + * @param {String|Blob|File} val + * @return {Request} for chaining + * @api public + */ + + Request.prototype.field = function (name, val){ + if (!this._formData) this._formData = new FormData() + this._formData.append(name, val) + return this + } + +/** + * Queue the given `file` as an attachment to the specified `field`, + * with optional `filename`. + * + * ``` js + * request.post('/upload') + * .attach(new Blob(['hey!'], { type: "text/html"})) + * .end(callback); + * ``` + * + * @param {String} field + * @param {Blob|File} file + * @param {String} filename + * @return {Request} for chaining + * @api public + */ + + Request.prototype.attach = function (field, file, filename){ + if (!this._formData) this._formData = new FormData() + this._formData.append(field, file, filename) + return this + } + +/** + * Send `data`, defaulting the `.type()` to "json" when + * an object is given. + * + * Examples: + * + * // querystring + * request.get('/search') + * .end(callback) + * + * // multiple data "writes" + * request.get('/search') + * .send({ search: 'query' }) + * .send({ range: '1..5' }) + * .send({ order: 'desc' }) + * .end(callback) + * + * // manual json + * request.post('/user') + * .type('json') + * .send('{"name":"tj"}) + * .end(callback) + * + * // auto json + * request.post('/user') + * .send({ name: 'tj' }) + * .end(callback) + * + * // manual x-www-form-urlencoded + * request.post('/user') + * .type('form') + * .send('name=tj') + * .end(callback) + * + * // auto x-www-form-urlencoded + * request.post('/user') + * .type('form') + * .send({ name: 'tj' }) + * .end(callback) + * + * // defaults to x-www-form-urlencoded + * request.post('/user') + * .send('name=tobi') + * .send('species=ferret') + * .end(callback) + * + * @param {String|Object} data + * @return {Request} for chaining + * @api public + */ + + Request.prototype.send = function (data){ + var obj = isObject(data) + var type = this.getHeader('Content-Type') + + // merge + if (obj && isObject(this._data)) { + for (var key in data) { + this._data[key] = data[key] + } + } else if ('string' === typeof data) { + if (!type) this.type('form') + type = this.getHeader('Content-Type') + if ('application/x-www-form-urlencoded' === type) { + this._data = this._data + ? this._data + '&' + data + : data + } else { + this._data = (this._data || '') + data + } + } else { + this._data = data + } + + if (!obj) return this + if (!type) this.type('json') + return this + } + +/** + * Invoke the callback with `err` and `res` + * and handle arity check. + * + * @param {Error} err + * @param {Response} res + * @api private + */ + + Request.prototype.callback = function (err, res){ + var fn = this._callback + this.clearTimeout() + if (2 === fn.length) return fn(err, res) + if (err) return this.emit('error', err) + fn(res) + } + +/** + * Invoke callback with x-domain error. + * + * @api private + */ + + Request.prototype.crossDomainError = function (){ + var err = new Error('Origin is not allowed by Access-Control-Allow-Origin') + err.crossDomain = true + this.callback(err) + } + +/** + * Invoke callback with timeout error. + * + * @api private + */ + + Request.prototype.timeoutError = function (){ + var timeout = this._timeout + var err = new Error('timeout of ' + timeout + 'ms exceeded') + err.timeout = timeout + this.callback(err) + } + +/** + * Enable transmission of cookies with x-domain requests. + * + * Note that for this to work the origin must not be + * using "Access-Control-Allow-Origin" with a wildcard, + * and also must set "Access-Control-Allow-Credentials" + * to "true". + * + * @api public + */ + + Request.prototype.withCredentials = function (){ + this._withCredentials = true + return this + } + +/** + * Initiate request, invoking callback `fn(res)` + * with an instanceof `Response`. + * + * @param {Function} fn + * @return {Request} for chaining + * @api public + */ + + Request.prototype.end = function (fn){ + var self = this + var xhr = this.xhr = getXHR() + var query = this._query.join('&') + var timeout = this._timeout + var data = this._formData || this._data + + // store callback + this._callback = fn || noop + + // state change + xhr.onreadystatechange = function (){ + if (4 != xhr.readyState) return + if (0 === xhr.status) { + if (self.aborted) return self.timeoutError() + return self.crossDomainError() + } + self.emit('end') + } + + // progress + if (xhr.upload) { + xhr.upload.onprogress = function (e){ + e.percent = e.loaded / e.total * 100 + self.emit('progress', e) + } + } + + // timeout + if (timeout && !this._timer) { + this._timer = setTimeout(function (){ + self.abort() + }, timeout) + } + + // querystring + if (query) { + query = request.serializeObject(query) + this.url += ~this.url.indexOf('?') + ? '&' + query + : '?' + query + } + + // initiate request + xhr.open(this.method, this.url, true) + + // CORS + if (this._withCredentials) xhr.withCredentials = true + + // body + if ('GET' != this.method && 'HEAD' != this.method && 'string' != typeof data && !isHost(data)) { + // serialize stuff + var serialize = request.serialize[this.getHeader('Content-Type')] + if (serialize) data = serialize(data) + } + + // set header fields + for (var field in this.header) { + if (null == this.header[field]) continue + xhr.setRequestHeader(field, this.header[field]) + } + + // send stuff + this.emit('request', this) + xhr.send(data) + return this + } + +/** + * Expose `Request`. + */ + + request.Request = Request + +/** + * Issue a request: + * + * Examples: + * + * request('GET', '/users').end(callback) + * request('/users').end(callback) + * request('/users', callback) + * + * @param {String} method + * @param {String|Function} url or callback + * @return {Request} + * @api public + */ + + function request (method, url) { + // callback + if ('function' === typeof url) { + return new Request('GET', method).end(url) + } + + // url first + if (1 === arguments.length) { + return new Request('GET', method) + } + + return new Request(method, url) + } + +/** + * GET `url` with optional callback `fn(res)`. + * + * @param {String} url + * @param {Mixed|Function} data or fn + * @param {Function} fn + * @return {Request} + * @api public + */ + + request.get = function (url, data, fn){ + var req = request('GET', url) + if ('function' === typeof data) fn = data, data = null + if (data) req.query(data) + if (fn) req.end(fn) + return req + } + +/** + * HEAD `url` with optional callback `fn(res)`. + * + * @param {String} url + * @param {Mixed|Function} data or fn + * @param {Function} fn + * @return {Request} + * @api public + */ + + request.head = function (url, data, fn){ + var req = request('HEAD', url) + if ('function' === typeof data) fn = data, data = null + if (data) req.send(data) + if (fn) req.end(fn) + return req + } + +/** + * DELETE `url` with optional callback `fn(res)`. + * + * @param {String} url + * @param {Function} fn + * @return {Request} + * @api public + */ + + request.del = function (url, fn){ + var req = request('DELETE', url) + if (fn) req.end(fn) + return req + } + +/** + * PATCH `url` with optional `data` and callback `fn(res)`. + * + * @param {String} url + * @param {Mixed} data + * @param {Function} fn + * @return {Request} + * @api public + */ + + request.patch = function (url, data, fn){ + var req = request('PATCH', url) + if ('function' === typeof data) fn = data, data = null + if (data) req.send(data) + if (fn) req.end(fn) + return req + } + +/** + * POST `url` with optional `data` and callback `fn(res)`. + * + * @param {String} url + * @param {Mixed} data + * @param {Function} fn + * @return {Request} + * @api public + */ + + request.post = function (url, data, fn){ + var req = request('POST', url) + if ('function' === typeof data) fn = data, data = null + if (data) req.send(data) + if (fn) req.end(fn) + return req + } + +/** + * PUT `url` with optional `data` and callback `fn(res)`. + * + * @param {String} url + * @param {Mixed|Function} data or fn + * @param {Function} fn + * @return {Request} + * @api public + */ + + request.put = function (url, data, fn){ + var req = request('PUT', url) + if ('function' === typeof data) fn = data, data = null + if (data) req.send(data) + if (fn) req.end(fn) + return req + } + +/** + * Expose `request`. + */ + + module.exports = request + + }) + + require.alias('component-emitter/index.js', 'superagent/deps/emitter/index.js') + require.alias('component-emitter/index.js', 'emitter/index.js') + + require.alias('component-reduce/index.js', 'superagent/deps/reduce/index.js') + require.alias('component-reduce/index.js', 'reduce/index.js') + + require.alias('superagent/lib/client.js', 'superagent/index.js');if (typeof exports === 'object') { + module.exports = require('superagent') + } else if (typeof define === 'function' && define.amd) { + define([], function (){ return require('superagent') }) + } else { + this['superagent'] = require('superagent') + }})() diff --git a/dist/assets/verdana.ttf b/dist/assets/verdana.ttf new file mode 100644 index 0000000..8f25a64 Binary files /dev/null and b/dist/assets/verdana.ttf differ diff --git a/dist/badge.js b/dist/badge.js new file mode 100644 index 0000000..3341793 --- /dev/null +++ b/dist/badge.js @@ -0,0 +1,43 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = badge; + +var _vd = require('vd'); + +var _vd2 = _interopRequireDefault(_vd); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var title = 'slack'; +var color = '#E01563'; +var pad = 8; // left / right padding +var sep = 4; // middle separation + +function badge(_ref) { + var total = _ref.total; + var active = _ref.active; + + var value = active ? active + '/' + total : '' + total || '–'; + var lw = pad + width(title) + sep; // left side width + var rw = sep + width(value) + pad; // right side width + var tw = lw + rw; // total width + + return (0, _vd2.default)('svg xmlns="http://www.w3.org/2000/svg" width=' + tw + ' height=20', (0, _vd2.default)('rect rx=3 width=' + tw + ' height=20 fill=#555'), (0, _vd2.default)('rect rx=3 x=' + lw + ' width=' + rw + ' height=20 fill=' + color), (0, _vd2.default)('path d="M' + lw + ' 0h' + sep + 'v20h-' + sep + 'z" fill=' + color), (0, _vd2.default)('g text-anchor=middle font-family=Verdana font-size=11', text({ str: title, x: Math.round(lw / 2), y: 14 }), text({ str: value, x: lw + Math.round(rw / 2), y: 14 }))); +} + +// generate text with 1px shadow +function text(_ref2) { + var str = _ref2.str; + var x = _ref2.x; + var y = _ref2.y; + + return [(0, _vd2.default)('text fill=#010101 fill-opacity=.3 x=' + x + ' y=' + (y + 1), str), (0, _vd2.default)('text fill=#fff x=' + x + ' y=' + y, str)]; +} + +// π=3 +function width(str) { + return 7 * str.length; +} \ No newline at end of file diff --git a/dist/iframe.js b/dist/iframe.js new file mode 100644 index 0000000..0d883fb --- /dev/null +++ b/dist/iframe.js @@ -0,0 +1,45 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = iframe; + +var _vd = require('vd'); + +var _vd2 = _interopRequireDefault(_vd); + +var _fs = require('fs'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var logo = (0, _fs.readFileSync)(__dirname + '/assets/slack.svg').toString('base64'); +var js = (0, _fs.readFileSync)(__dirname + '/assets/iframe.js').toString(); +var css = (0, _fs.readFileSync)(__dirname + '/assets/iframe-button.css').toString(); + +function iframe(_ref) { + var path = _ref.path; + var active = _ref.active; + var total = _ref.total; + var large = _ref.large; + + var str = ''; + if (active) str = active + '/'; + if (total) str += total; + if (!str.length) str = '–'; + + var opts = { 'class': large ? 'slack-btn-large' : '' }; + var div = (0, _vd2.default)('span.slack-button', opts, (0, _vd2.default)('a.slack-btn href=/ target=_blank', (0, _vd2.default)('span.slack-ico'), (0, _vd2.default)('span.slack-text', 'Slack')), (0, _vd2.default)('a.slack-count href=/ target=_blank', str), (0, _vd2.default)('style', css), _vd2.default.style().add('.slack-ico', { + 'background-image': 'url(data:image/svg+xml;base64,' + logo + ')' + }), (0, _vd2.default)('script', '\n data = {};\n data.path = ' + JSON.stringify(path) + ';\n data.total = ' + (total != null ? total : 'null') + ';\n data.active = ' + (active != null ? active : 'null') + ';\n '), (0, _vd2.default)('script', js)); + + return div; +} + +function gradient(css, sel, params) { + ['-webkit-', '-moz-', ''].forEach(function (p) { + css.add(sel, { + 'background-image': p + 'linear-gradient(' + params + ')' + }); + }); +} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..54f2cce --- /dev/null +++ b/dist/index.js @@ -0,0 +1,244 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = slackin; + +require('babel-polyfill'); + +var _express = require('express'); + +var _express2 = _interopRequireDefault(_express); + +var _socket = require('socket.io'); + +var _socket2 = _interopRequireDefault(_socket); + +var _bodyParser = require('body-parser'); + +var _http = require('http'); + +var _emailRegex = require('email-regex'); + +var _emailRegex2 = _interopRequireDefault(_emailRegex); + +var _vd = require('vd'); + +var _vd2 = _interopRequireDefault(_vd); + +var _cors = require('cors'); + +var _cors2 = _interopRequireDefault(_cors); + +var _slack = require('./slack'); + +var _slack2 = _interopRequireDefault(_slack); + +var _slackInvite = require('./slack-invite'); + +var _slackInvite2 = _interopRequireDefault(_slackInvite); + +var _badge = require('./badge'); + +var _badge2 = _interopRequireDefault(_badge); + +var _splash = require('./splash'); + +var _splash2 = _interopRequireDefault(_splash); + +var _iframe = require('./iframe'); + +var _iframe2 = _interopRequireDefault(_iframe); + +var _log = require('./log'); + +var _log2 = _interopRequireDefault(_log); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// our code +// es6 runtime requirements +function slackin(_ref) { + var token = _ref.token; + var _ref$interval = _ref.interval; + var interval = _ref$interval === undefined ? 5000 : _ref$interval; + var org = _ref.org; + var css = _ref.css; + var coc = _ref.coc; + var _ref$cors = _ref.cors; + var useCors = _ref$cors === undefined ? false : _ref$cors; + var _ref$path = _ref.path; + var path = _ref$path === undefined ? '/' : _ref$path; + var channels = _ref.channels; + var _ref$silent = _ref.silent; + var silent = _ref$silent === undefined ? false : _ref$silent; + + // must haves + if (!token) throw new Error('Must provide a `token`.'); + if (!org) throw new Error('Must provide an `org`.'); + + if (channels) { + // convert to an array + channels = channels.split(',').map(function (channel) { + // sanitize channel name + if ('#' === channel[0]) return channel.substr(1); + return channel; + }); + } + + // setup app + var app = (0, _express2.default)(); + var srv = (0, _http.Server)(app); + srv.app = app; + + var assets = __dirname + '/assets'; + + // fetch data + var slack = new _slack2.default({ token: token, interval: interval, org: org }); + + slack.setMaxListeners(Infinity); + + // capture stats + (0, _log2.default)(slack, silent); + + // middleware for waiting for slack + app.use(function (req, res, next) { + if (slack.ready) return next(); + slack.once('ready', next); + }); + + if (useCors) { + app.options('*', (0, _cors2.default)()); + app.use((0, _cors2.default)()); + } + + // splash page + app.get('/', function (req, res) { + var _slack$org = slack.org; + var name = _slack$org.name; + var logo = _slack$org.logo; + var _slack$users = slack.users; + var active = _slack$users.active; + var total = _slack$users.total; + + if (!name) return res.send(404); + var page = (0, _vd2.default)('html', (0, _vd2.default)('head', (0, _vd2.default)('title', 'Join ', name, ' on Slack!'), (0, _vd2.default)('meta name=viewport content="width=device-width,initial-scale=1.0,minimum-scale=1.0,user-scalable=no"'), (0, _vd2.default)('link rel="shortcut icon" href=https://slack.global.ssl.fastly.net/272a/img/icons/favicon-32.png'), css && (0, _vd2.default)('link rel=stylesheet', { href: css })), (0, _splash2.default)({ coc: coc, path: path, css: css, name: name, org: org, logo: logo, channels: channels, active: active, total: total })); + res.type('html'); + res.send(page.toHTML()); + }); + + app.get('/data', function (req, res) { + var _slack$org2 = slack.org; + var name = _slack$org2.name; + var logo = _slack$org2.logo; + var _slack$users2 = slack.users; + var active = _slack$users2.active; + var total = _slack$users2.total; + + res.send({ + name: name, + org: org, + coc: coc, + logo: logo, + channels: channels, + active: active, + total: total + }); + }); + + // static files + app.use('/assets', _express2.default.static(assets)); + + // invite endpoint + app.post('/invite', (0, _bodyParser.json)(), function (req, res, next) { + var chanId = void 0; + if (channels) { + var channel = req.body.channel; + if (!channels.includes(channel)) { + return res.status(400).json({ msg: 'Not a permitted channel' }); + } + chanId = slack.getChannelId(channel); + if (!chanId) { + return res.status(400).json({ msg: 'Channel not found "' + channel + '"' }); + } + } + + var email = req.body.email; + + if (!email) { + return res.status(400).json({ msg: 'No email provided' }); + } + + if (!(0, _emailRegex2.default)().test(email)) { + return res.status(400).json({ msg: 'Invalid email' }); + } + + if (coc && '1' != req.body.coc) { + return res.status(400).json({ msg: 'Agreement to CoC is mandatory' }); + } + + (0, _slackInvite2.default)({ token: token, org: org, email: email, channel: chanId }, function (err) { + if (err) { + if (err.message === 'Sending you to Slack...') { + return res.status(303).json({ msg: err.message, redirectUrl: 'https://' + org + '.slack.com' }); + } + + return res.status(400).json({ msg: err.message }); + } + + res.status(200).json({ msg: 'WOOT. Check your email!' }); + }); + }); + + // iframe + app.get('/iframe', function (req, res) { + var large = 'large' in req.query; + var _slack$users3 = slack.users; + var active = _slack$users3.active; + var total = _slack$users3.total; + + res.type('html'); + res.send((0, _iframe2.default)({ path: path, active: active, total: total, large: large }).toHTML()); + }); + + app.get('/iframe/dialog', function (req, res) { + var large = 'large' in req.query; + var name = slack.org.name; + var _slack$users4 = slack.users; + var active = _slack$users4.active; + var total = _slack$users4.total; + + if (!name) return res.send(404); + var dom = (0, _splash2.default)({ coc: coc, path: path, name: name, org: org, channels: channels, active: active, total: total, large: large, iframe: true }); + res.type('html'); + res.send(dom.toHTML()); + }); + + // badge js + app.use('/slackin.js', _express2.default.static(assets + '/badge.js')); + + // badge rendering + app.get('/badge.svg', function (req, res) { + res.type('svg'); + res.set('Cache-Control', 'max-age=0, no-cache'); + res.set('Pragma', 'no-cache'); + res.send((0, _badge2.default)(slack.users).toHTML()); + }); + + // realtime + (0, _socket2.default)(srv).on('connection', function (socket) { + socket.emit('data', slack.users); + var change = function change(key, val) { + return socket.emit(key, val); + }; + slack.on('change', change); + socket.on('disconnect', function () { + slack.removeListener('change', change); + }); + }); + + return srv; +} + +// their code \ No newline at end of file diff --git a/dist/log.js b/dist/log.js new file mode 100644 index 0000000..bd88578 --- /dev/null +++ b/dist/log.js @@ -0,0 +1,69 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = log; + +var _debug = require('debug'); + +var _debug2 = _interopRequireDefault(_debug); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var debug = (0, _debug2.default)('slackin'); + +function log(slack, silent) { + // keep track of elapsed time + var last = void 0; + + out('fetching'); + + // attach events + slack.on('ready', function () { + return out('ready'); + }); + slack.on('retry', function () { + return out('retrying'); + }); + + slack.on('fetch', function () { + last = new Date(); + out('fetching'); + }); + + slack.on('data', online); + + // log online users + function online() { + out('online %d, total %d %s', slack.users.active, slack.users.total, last ? '(+' + (new Date() - last) + 'ms)' : ''); + } + + // print out errors and warnings + if (!silent) { + slack.on('error', function (err) { + console.error('%s – ' + err.stack, new Date()); + }); + + slack.on('ready', function () { + if (!slack.org.logo && !silent) { + console.warn('\u001b[92mWARN: no logo configured\u001b[39m'); + } + }); + } + + function out() { + var _console; + + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + if (args) { + args[0] = new Date() + ' – ' + args[0]; + } + + if (silent) return debug.apply(undefined, args); + (_console = console).log.apply(_console, args); + } +} \ No newline at end of file diff --git a/dist/popup.js b/dist/popup.js new file mode 100644 index 0000000..9a390c3 --- /dev/null +++ b/dist/popup.js @@ -0,0 +1 @@ +"use strict"; \ No newline at end of file diff --git a/dist/slack-invite.js b/dist/slack-invite.js new file mode 100644 index 0000000..9c2290c --- /dev/null +++ b/dist/slack-invite.js @@ -0,0 +1,59 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = invite; + +var _superagent = require('superagent'); + +var _superagent2 = _interopRequireDefault(_superagent); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function invite(_ref, fn) { + var org = _ref.org; + var token = _ref.token; + var email = _ref.email; + var channel = _ref.channel; + + var data = { email: email, token: token }; + + if (channel) { + data.channels = channel; + data.ultra_restricted = 1; + data.set_active = true; + } + + _superagent2.default.post('https://' + org + '.slack.com/api/users.admin.invite').type('form').send(data).end(function (err, res) { + if (err) return fn(err); + if (200 != res.status) { + fn(new Error('Invalid response ' + res.status + '.')); + return; + } + + // If the account that owns the token is not admin, Slack will oddly + // return `200 OK`, and provide other information in the body. So we + // need to check for the correct account scope and call the callback + // with an error if it's not high enough. + var _res$body = res.body; + var ok = _res$body.ok; + var providedError = _res$body.error; + var needed = _res$body.needed; + + if (!ok) { + if (providedError === 'missing_scope' && needed === 'admin') { + fn(new Error('Missing admin scope: The token you provided is for an account that is not an admin. You must provide a token from an admin account in order to invite users through the Slack API.')); + } else if (providedError === 'already_invited') { + fn(new Error('You have already been invited to Slack. Check for an email from feedback@slack.com.')); + } else if (providedError === 'already_in_team') { + fn(new Error('Sending you to Slack...')); + } else { + fn(new Error(providedError)); + } + return; + } + + fn(null); + }); +} \ No newline at end of file diff --git a/dist/slack.js b/dist/slack.js new file mode 100644 index 0000000..0fe8343 --- /dev/null +++ b/dist/slack.js @@ -0,0 +1,144 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _superagent = require('superagent'); + +var _superagent2 = _interopRequireDefault(_superagent); + +var _events = require('events'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var SlackData = function (_EventEmitter) { + _inherits(SlackData, _EventEmitter); + + function SlackData(_ref) { + var token = _ref.token; + var interval = _ref.interval; + var host = _ref.org; + + _classCallCheck(this, SlackData); + + var _this = _possibleConstructorReturn(this, (SlackData.__proto__ || Object.getPrototypeOf(SlackData)).call(this)); + + _this.host = host; + _this.token = token; + _this.interval = interval; + _this.ready = false; + _this.org = {}; + _this.users = {}; + _this.channelsByName = {}; + _this.init(); + _this.fetch(); + return _this; + } + + _createClass(SlackData, [{ + key: 'init', + value: function init() { + var _this2 = this; + + _superagent2.default.get('https://' + this.host + '.slack.com/api/channels.list').query({ token: this.token }).end(function (err, res) { + (res.body.channels || []).forEach(function (channel) { + _this2.channelsByName[channel.name] = channel; + }); + }); + + _superagent2.default.get('https://' + this.host + '.slack.com/api/team.info').query({ token: this.token }).end(function (err, res) { + var team = res.body.team; + if (!team) { + throw new Error('Bad Slack response. Make sure the team name and API keys are correct'); + } + _this2.org.name = team.name; + if (!team.icon.image_default) { + _this2.org.logo = team.icon.image_132; + } + }); + } + }, { + key: 'fetch', + value: function fetch() { + var _this3 = this; + + _superagent2.default.get('https://' + this.host + '.slack.com/api/users.list').query({ token: this.token, presence: 1 }).end(function (err, res) { + _this3.onres(err, res); + }); + this.emit('fetch'); + } + }, { + key: 'getChannelId', + value: function getChannelId(name) { + var channel = this.channelsByName[name]; + return channel ? channel.id : null; + } + }, { + key: 'retry', + value: function retry() { + var interval = this.interval * 2; + setTimeout(this.fetch.bind(this), interval); + this.emit('retry'); + } + }, { + key: 'onres', + value: function onres(err, res) { + if (err) { + this.emit('error', err); + return this.retry(); + } + + var users = res.body.members; + + if (!users) { + var _err = new Error('Invalid Slack response: ' + res.status); + this.emit('error', _err); + return this.retry(); + } + + // remove slackbot and bots from users + // slackbot is not a bot, go figure! + users = users.filter(function (x) { + return x.id != 'USLACKBOT' && !x.is_bot && !x.deleted; + }); + + var total = users.length; + var active = users.filter(function (user) { + return 'active' === user.presence; + }).length; + + if (this.users) { + if (total != this.users.total) { + this.emit('change', 'total', total); + } + if (active != this.users.active) { + this.emit('change', 'active', active); + } + } + + this.users.total = total; + this.users.active = active; + + if (!this.ready) { + this.ready = true; + this.emit('ready'); + } + + setTimeout(this.fetch.bind(this), this.interval); + this.emit('data'); + } + }]); + + return SlackData; +}(_events.EventEmitter); + +exports.default = SlackData; \ No newline at end of file diff --git a/dist/splash.js b/dist/splash.js new file mode 100644 index 0000000..e743afd --- /dev/null +++ b/dist/splash.js @@ -0,0 +1,320 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = splash; + +var _vd = require('vd'); + +var _vd2 = _interopRequireDefault(_vd); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function splash(_ref) { + var path = _ref.path; + var name = _ref.name; + var org = _ref.org; + var coc = _ref.coc; + var logo = _ref.logo; + var active = _ref.active; + var total = _ref.total; + var channels = _ref.channels; + var large = _ref.large; + var iframe = _ref.iframe; + + var div = (0, _vd2.default)('.splash', !iframe && (0, _vd2.default)('.logos', logo && (0, _vd2.default)('.logo.org'), (0, _vd2.default)('.logo.slack')), (0, _vd2.default)('p', 'Join ', (0, _vd2.default)('b', name), + // mention single single-channel inline + channels && channels.length === 1 && (0, _vd2.default)('span', ' #', channels[0]), ' on Slack.'), (0, _vd2.default)('p.status', active ? [(0, _vd2.default)('b.active', active), ' users online now of ', (0, _vd2.default)('b.total', total), ' registered.'] : [(0, _vd2.default)('b.total', total), ' users are registered so far.']), (0, _vd2.default)('form id=invite', channels && (channels.length > 1 + // channel selection when there are multiple + ? (0, _vd2.default)('select.form-item name=channel', channels.map(function (channel) { + return (0, _vd2.default)('option', { value: channel, text: channel }); + })) + // otherwise a fixed channel + : (0, _vd2.default)('input type=hidden name=channel', { value: channels[0] })), (0, _vd2.default)('input.form-item type=email name=email placeholder=you@yourdomain.com ' + (!iframe ? 'autofocus' : '')), coc && (0, _vd2.default)('.coc', (0, _vd2.default)('label', (0, _vd2.default)('input type=checkbox name=coc value=1'), 'I agree to the ', (0, _vd2.default)('a', { href: coc, target: '_blank' }, 'Code of Conduct'), '.')), (0, _vd2.default)('button.loading', 'Get my Invite')), (0, _vd2.default)('p.signin', 'or ', (0, _vd2.default)('a href=https://' + org + '.slack.com target=_top', 'sign in'), '.'), !iframe && (0, _vd2.default)('footer', 'powered by ', (0, _vd2.default)('a href=http://rauchg.com/slackin target=_blank', 'slackin')), style({ logo: logo, active: active, large: large, iframe: iframe }), + // xxx: single build + (0, _vd2.default)('script', '\n data = {};\n data.path = ' + JSON.stringify(path) + ';\n '), (0, _vd2.default)('script src=https://cdn.socket.io/socket.io-1.4.4.js'), (0, _vd2.default)('script src=' + path + 'assets/superagent.js'), (0, _vd2.default)('script src=' + path + 'assets/client.js')); + return div; +} + +var pink = '#E01563'; + +function style() { + var _ref2 = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; + + var logo = _ref2.logo; + var active = _ref2.active; + var large = _ref2.large; + var iframe = _ref2.iframe; + + var css = _vd2.default.style(); + css.add('html', { 'font-size': large ? '14px' : '10px' }); + + css.add('.splash', { + 'width': iframe ? '25rem' : '30rem', + 'margin': iframe ? '0' : '20rem auto', + 'text-align': 'center', + 'font-family': '"Helvetica Neue", Helvetica, Arial' + }); + + if (iframe) { + css.add('body, html', { + 'margin': '0', + 'padding': '0', + 'background': '#FAFAFA', + 'overflow': 'hidden' // ff + }); + + css.add('.splash', { + 'box-sizing': 'border-box', + 'padding': '1rem' + }); + } + + if (!iframe) { + css.media('(max-width: 50rem)').add('.splash', { + 'margin-top': '10rem' + }); + } + + css.add('.head', { + 'margin-bottom': '4rem' + }); + + css.add('.logos', { + 'position': 'relative', + 'margin-bottom': '4rem' + }); + + if (!iframe) { + css.add('.logo', { + 'width': '4.8rem', + 'height': '4.8rem', + 'display': 'inline-block', + 'background-size': 'cover' + }); + + css.add('.logo.slack', { + 'background-image': 'url(assets/slack.svg)' + }); + + if (logo) { + var pw = 1; // '+' width in rem + var lp = 3; // logos separation in rem + + css.add('.logo.org::after', { + 'position': 'absolute', + 'display': 'block', + 'content': '"+"', + 'top': '1.5rem', + 'left': '0', + 'width': '30rem', + 'text-align': 'center', + 'color': '#D6D6D6', + 'font-size': '1.5rem', // can't use rem in font shorthand IE9-10 + // http://codersblock.com/blog/font-shorthand-bug-in-ie10/ + 'font-family': 'Helvetica Neue' + }); + + css.add('.logo.org', { + 'background-image': 'url(' + logo + ')', + 'margin-right': lp + pw + lp + 'rem' + }); + } + } + + css.add('.coc', { + 'font-size': '1.2rem', + padding: '1.5rem 0 .5rem', + color: '#666' + }); + + if (iframe) { + css.add('.coc', { + 'font-size': '1.1rem', + 'padding-top': '1rem' + }); + + css.add('.coc input', { + position: 'relative', + top: '-.2rem' + }); + } + + css.add('.coc label', { + cursor: 'pointer' + }); + + css.add('.coc input', { + 'appearance': 'none', + '-webkit-appearance': 'none', + border: 'none', + 'vertical-align': 'middle', + margin: '0 .5rem 0 0' + }); + + css.add('.coc input::after', { + content: '""', + display: 'inline-block', + width: '1.5rem', + height: '1.5rem', + 'vertical-align': 'middle', + background: 'url(/assets/checkbox.svg)', + cursor: 'pointer' + }); + + css.add('.coc input:checked::after', { + 'background-position': 'right' + }); + + css.add('.coc a', { + color: '#666' + }); + + css.add('.coc a:hover', { + 'background-color': '#666', + 'text-decoration': 'none', + color: '#fff' + }); + + css.add('p', { + 'font-size': iframe ? '1.2rem' : '1.5rem', + 'margin': iframe ? '0 0 .5rem' : '.5rem 0' + }); + + if (iframe) { + css.add('p.status', { + 'font-size': '1.1rem' + }); + } + + css.add('select', { + 'background': 'none' + }); + + css.add('button, .form-item', { + 'font-size': '1.2rem', + 'margin-top': iframe ? '.5rem' : '1rem', + 'vertical-align': 'middle', + 'display': 'block', + 'text-align': 'center', + 'box-sizing': 'border-box', + 'width': '100%', + 'padding': '.9rem' + }); + + css.add('button', { + 'color': '#fff', + 'font-weight': 'bold', + 'border-width': 0, + 'background': pink, + 'text-transform': 'uppercase', + 'cursor': 'pointer', + 'appearence': 'none', + '-webkit-appearence': 'none', + 'outline': '0', + 'transition': 'background-color 150ms ease-in, color 150ms ease-in' + }); + + css.add('button.loading', { + 'pointer-events': 'none' + }); + + css.add('button:disabled', { + 'color': '#9B9B9B', + 'background-color': '#D6D6D6', + 'cursor': 'default', + 'pointer-events': 'none' + }); + + css.add('button.error', { + 'background-color': '#F4001E', + 'text-transform': 'none' + }); + + css.add('button.success:disabled', { + 'color': '#fff', + 'background-color': '#68C200' + }); + + css.add('button:not(.disabled):active', { + 'background-color': '#7A002F' + }); + + css.add('b', { + 'transition': 'transform 150ms ease-in' + }); + + css.add('b.grow', { + 'transform': 'scale(1.3)' + }); + + css.add('form', { + 'margin-top': iframe ? '1rem' : '2rem', + 'margin-bottom': '0' + }); + + css.add('input', { + 'color': '#9B9B9B', + 'border': '.1rem solid #D6D6D6' + }); + + if (iframe) { + css.add('button, .form-item', { + 'height': '2.8rem', + 'line-height': '2.8rem', + 'padding': 0, + 'font-size': '1.1rem' + }); + } + + css.add('input:focus', { + 'color': '#666', + 'border-color': '#999', + 'outline': '0' + }); + + if (active) { + css.add('.active', { + 'color': pink + }); + } + + css.add('p.signin', { + 'padding': '1rem 0 1rem', + 'font-size': '1.1rem' + }); + + css.add('p.signin a', { + 'color': pink, + 'text-decoration': 'none' + }); + + css.add('p.signin a:hover', { + 'background-color': '#E01563', + color: '#fff' + }); + + if (!iframe) { + css.add('footer', { + 'color': '#D6D6D6', + 'font-size': '1.1rem', + 'margin': '20rem auto 0', + 'width': '30rem', + 'text-align': 'center' + }); + + css.add('footer a', { + 'color': '#9B9B9B', + 'text-decoration': 'none', + 'border-bottom': '.1rem solid #9B9B9B' + }); + + css.add('footer a:hover', { + 'color': '#fff', + 'background-color': '#9B9B9B' + }); + } + + return css; +} \ No newline at end of file