├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── marmelada ├── cli.js ├── index.js ├── package.json ├── screenshot.gif └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "browser": true 5 | }, 6 | 7 | "plugins": [ 8 | "no-use-extend-native" 9 | ], 10 | 11 | "rules": { 12 | 13 | "no-dupe-args": 2, 14 | "no-dupe-keys": 2, 15 | "no-extra-semi": 2, 16 | "no-extra-parens": 2, 17 | "no-unreachable": 2, 18 | 19 | "curly": [2, "all"], 20 | "eqeqeq": [2, "smart"], 21 | "no-floating-decimal": 2, 22 | "no-multi-spaces": 2, 23 | "no-multi-str": 2, 24 | "no-redeclare": 2, 25 | "no-self-compare": 2, 26 | "no-sequences": 2, 27 | "no-useless-call": 2, 28 | "radix": 2, 29 | "vars-on-top": 2, 30 | "wrap-iife": [2, "outside"], 31 | 32 | "strict": [2, "global"], 33 | "no-shadow": 2, 34 | "no-undef-init": 2, 35 | "no-undef": 2, 36 | "no-unused-vars": [2, {"vars": "all", "args": "none"}], 37 | "no-use-before-define": 2, 38 | "no-new-require": 2, 39 | 40 | "brace-style": [2, "stroustrup"], 41 | "comma-spacing": [2, {"before": false, "after": true}], 42 | "comma-style": [2, "last"], 43 | "indent": [2, 2], 44 | "new-cap": 2, 45 | "new-parens": 2, 46 | "no-mixed-spaces-and-tabs": 2, 47 | "no-multiple-empty-lines": [2, {"max": 2}], 48 | "no-nested-ternary": 2, 49 | "no-new-object": 2, 50 | "no-spaced-func": 2, 51 | "no-trailing-spaces": 2, 52 | "no-unneeded-ternary": 2, 53 | "object-curly-spacing": [2, "always"], 54 | "one-var": [2, "never"], 55 | "operator-linebreak": [2, "after"], 56 | "quote-props": [2, "as-needed"], 57 | "quotes": [2, "single"], 58 | "semi-spacing": [2, {"before": false, "after": true}], 59 | "semi": [2, "always"], 60 | "space-before-function-paren": [2, "never"], 61 | "space-in-parens": [2, "never"], 62 | "space-infix-ops": 2, 63 | "spaced-comment": [2, "always"], 64 | "wrap-regex": 2 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS stuff 2 | .DS_Store 3 | .DS_Store? 4 | ._* 5 | .Spotlight-V100 6 | .Trashes 7 | Icon? 8 | ehthumbs.db 9 | Thumbs.db 10 | 11 | # Node.js stuff 12 | node_modules 13 | 14 | # Vim stuff 15 | *.swp 16 | *.swo 17 | 18 | # Source maps 19 | *.map 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'iojs' 4 | - '0.12' 5 | - '0.10' -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.0.0 (2015/08/31) 2 | 3 | * Initial release. 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Rafael Rinaldi (rinaldi.io) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # marmelada [![Build Status](https://travis-ci.org/rafaelrinaldi/marmelada.svg?branch=master)](https://travis-ci.org/rafaelrinaldi/marmelada) 2 | 3 | > Randomly select people for [FEMUG-SP](http://sp.femug.com) meetings. 4 | 5 | ![screenshot](./screenshot.gif) 6 | 7 | ## Install 8 | 9 | ```sh 10 | npm install -g marmelada 11 | ``` 12 | 13 | ## Usage 14 | 15 | ``` 16 | $ marmelada --help 17 | 18 | Usage: marmelada [URL] [OPTIONS] 19 | 20 | Randomly select people for FEMUG-SP's meetings. 21 | 22 | Example: 23 | marmelada http://sp.femug.com/t/femug-42-nasa --total 10 24 | 25 | Options: 26 | -v --version Display current software version. 27 | -h --help Display help and usage details. 28 | -f --format Output format. 29 | -t --total Total spots available for the meeting (defaults to list size). 30 | -i --ignore Ignore list (comma separated and case sensitive). 31 | ``` 32 | 33 | ### Examples 34 | 35 | ```sh 36 | # List all participants 37 | $ marmelada http://sp.femug.com/t/thread-name/thread-id 38 | 39 | # Randomly select 10 users 40 | $ marmelada http://sp.femug.com/t/thread-name/thread-id -t 10 41 | 42 | # You can also ignore a list of users (case sensitive) 43 | $ marmelada http://sp.femug.com/t/thread-name/thread-id -i jarvis,BatMan,YOLO 44 | 45 | # Prefixing a list of users with "@" 46 | $ marmelada http://sp.femug.com/t/thread-name/thread-id -t 3 -f '@%s' 47 | 48 | @igorapa 49 | @marcelgsantos 50 | @keitoliveira 51 | ``` 52 | 53 | ### Options 54 | 55 | #### `-t`, `--total` 56 | 57 | Total spots available for the meeting. Without this argument `marmelada` will simply retrieve a list of all participants. 58 | 59 | #### `-i`, `--ignore` 60 | 61 | Comma-separated list of user names to keep out of the results. 62 | 63 | > By default moderators and admins participating on the thread will be ignored. 64 | 65 | #### `-f`, `--format` 66 | 67 | Format the entries list output. `%s` will be replaced by the user name. 68 | 69 | ## How it works 70 | 71 | There are a few simple steps in order to select users for [FEMUG-SP](http://sp.femug.com) meetings: 72 | 73 | 1. An admin creates a new forum thread with the details for the next meeting (company hosting, address, maximum capacity, etc); 74 | 2. Everyone interested on participating must reply the thread showing their interest; 75 | 3. If the number of people interested is less than the maximum capacity, a random list of people will be selected. 76 | 77 | ## License 78 | 79 | MIT © [Rafael Rinaldi](http://rinaldi.io) 80 | -------------------------------------------------------------------------------- /bin/marmelada: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var cli = require('../cli'); 6 | var argv = process.argv.slice(2); 7 | var options = cli.parse(argv); 8 | 9 | cli.run(options); 10 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var marmelada = require('./'); 4 | var minimist = require('minimist'); 5 | var multiline = require('multiline'); 6 | var defaults = { 7 | boolean: [ 8 | 'help', 9 | 'version' 10 | ], 11 | alias: { 12 | h: 'help', 13 | v: 'version', 14 | f: 'format', 15 | t: 'total', 16 | i: 'ignore' 17 | } 18 | }; 19 | var version = require('./package.json').version; 20 | var help = multiline(function() {/* 21 | 22 | Usage: marmelada [URL] [OPTIONS] 23 | 24 | Randomly select people for FEMUG-SP's meetings. 25 | 26 | Example: 27 | marmelada http://sp.femug.com/t/femug-42-nasa --total 10 28 | 29 | Options: 30 | -v --version Display current software version. 31 | -h --help Display help and usage details. 32 | -f --format Output format. 33 | -t --total Total spots available for the meeting (defaults to list size). 34 | -i --ignore Ignore list (comma separated and case sensitive). 35 | 36 | */}); 37 | 38 | function run(argv) { 39 | var url = argv._[0]; 40 | 41 | if(!url) { 42 | exports.stderr.write('You must provide at least an URL'); 43 | exports.exitCode = 1; 44 | return; 45 | } 46 | 47 | marmelada(url, argv); 48 | } 49 | 50 | exports.exitCode = 0; 51 | 52 | exports.stdout = process.stdout; 53 | exports.stderr = process.stderr; 54 | 55 | exports.parse = function(options) { 56 | return minimist(options, defaults); 57 | }; 58 | 59 | exports.run = function(argv) { 60 | exports.exitCode = 0; 61 | 62 | if(argv.help) { 63 | exports.stderr.write(help); 64 | return; 65 | } 66 | 67 | if(argv.version) { 68 | exports.stderr.write('marmelada v' + version + '\n'); 69 | return; 70 | } 71 | 72 | run(argv); 73 | }; 74 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('request'); 4 | var _ = require('lodash'); 5 | var sprintf = require('util').format; 6 | var logUpdate = require('log-update'); 7 | var spinner = require('elegant-spinner'); 8 | var frame = spinner(); 9 | var organizers = ['rafaelrinaldi', 'lucasmazza', 'danielfilho']; 10 | var ignore = organizers; 11 | var options = {}; 12 | var animation; 13 | 14 | function loading() { 15 | // Will start a fancy loading animation 16 | frame = spinner(); 17 | animation = setInterval(function() { 18 | logUpdate(frame()); 19 | }, 100); 20 | } 21 | 22 | function loaded() { 23 | clearInterval(animation); 24 | logUpdate(''); 25 | } 26 | 27 | function fetch(url) { 28 | var requestOptions = { 29 | /** 30 | * Big arbitrary number so we always get a fresh full list. 31 | * There's probably a better way to do it though. 32 | */ 33 | uri: url + '/999.json', 34 | method: 'GET', 35 | 36 | /** 37 | * Use this flag so we don't need to handle w/ cert. signature. 38 | * Ideally we should pass an API key and token, but that didn't worked at all. 39 | * Lots of people w/ the same issue, I've posted on Discourse's main forum to see what's up. 40 | */ 41 | rejectUnauthorized: false, 42 | json: true 43 | }; 44 | 45 | return new Promise(function(resolve, reject) { 46 | request(requestOptions, function(error, response, body){ 47 | if(error) { 48 | reject(error); 49 | } 50 | 51 | resolve(body); 52 | }); 53 | }); 54 | } 55 | 56 | function parse(data) { 57 | /** 58 | * For some reason the API don't always return all the participants. 59 | * So what I do here is to combine the results from "posts" and "participants". 60 | */ 61 | var posts = data.post_stream.posts; 62 | var replies = data.details.participants; 63 | var participants = posts.concat(replies); 64 | 65 | participants = _.pluck(participants, 'username'); 66 | participants = _.uniq(participants); 67 | 68 | if(options.ignore) { 69 | ignore = ignore.concat(options.ignore.replace(/\s/g, '').split(',')); 70 | } 71 | 72 | // Remove items that should be ignored if there are any 73 | _.remove(participants, function(participant) { 74 | return _.includes(ignore, participant); 75 | }); 76 | 77 | return participants; 78 | } 79 | 80 | function select(list) { 81 | // If "total" is not specified on the options, it will retrieve the full list 82 | var total = options.total < list.length ? options.total : list.length; 83 | return _.chain(list).shuffle().take(total).value(); 84 | } 85 | 86 | function format(list) { 87 | /** 88 | * User can customize the output. 89 | * By default it's the username followed by a new line. 90 | */ 91 | var template = options.format || '%s'; 92 | 93 | return list.map(function(item) { 94 | return sprintf(template, item) + '\n'; 95 | }).toString().replace(/\,/gm, '').trim(); 96 | } 97 | 98 | function output(input) { 99 | loaded(); 100 | // Output the result to stdout 101 | logUpdate(input); 102 | } 103 | 104 | module.exports = function(url, _options) { 105 | options = _options; 106 | loading(); 107 | 108 | fetch(url) 109 | .catch(function(error) { 110 | output(error); 111 | }) 112 | .then(parse) 113 | .then(select) 114 | .then(format) 115 | .then(output); 116 | }; 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marmelada", 3 | "version": "0.1.1", 4 | "description": "Randomly select people for FEMUG-SP meetings.", 5 | "license": "MIT", 6 | "repository": "rafaelrinaldi/marmelada", 7 | "bugs": { 8 | "url": "https://github.com/rafaelrinaldi/marmelada/issues" 9 | }, 10 | "homepage": "https://github.com/rafaelrinaldi/marmelada#readme", 11 | "author": { 12 | "name": "Rafael Rinaldi", 13 | "email": "rafael@rinaldi.io", 14 | "url": "http://rinaldi.io" 15 | }, 16 | "main": "index.js", 17 | "bin": { 18 | "marmelada": "./bin/marmelada" 19 | }, 20 | "engines": { 21 | "node": ">=0.10.0" 22 | }, 23 | "scripts": { 24 | "lint": "eslint .", 25 | "test": "npm run lint && tape test.js" 26 | }, 27 | "keywords": [ 28 | "femug", 29 | "femugsp", 30 | "sp", 31 | "sao paulo", 32 | "meetings", 33 | "draw", 34 | "random", 35 | "select" 36 | ], 37 | "dependencies": { 38 | "elegant-spinner": "^1.0.0", 39 | "lodash": "^3.10.1", 40 | "log-update": "^1.0.0", 41 | "minimist": "^1.2.0", 42 | "multiline": "^1.0.2", 43 | "object-assign": "^4.0.1", 44 | "request": "^2.61.0" 45 | }, 46 | "devDependencies": { 47 | "eslint": "^1.3.1", 48 | "eslint-plugin-no-use-extend-native": "*", 49 | "tape": "^4.2.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/femug/marmelada/344077ac4c2a4a9735d3fdb02a6371cb05930a42/screenshot.gif -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | 5 | test('marmelada test suite', function(t) { 6 | t.end(); 7 | }); 8 | --------------------------------------------------------------------------------