├── .babelrc ├── .gitignore ├── LICENSE.txt ├── Procfile ├── README.md ├── app.js ├── bower.json ├── client ├── css │ └── style.css └── scripts │ ├── app.jsx │ └── components │ ├── App.jsx │ ├── Cap.jsx │ ├── Dashboard.jsx │ ├── FollowedByList.jsx │ ├── FollowingList.jsx │ ├── GetFollowers.jsx │ ├── Login.jsx │ ├── Referrals.jsx │ ├── RemoveFollowers.jsx │ ├── User.jsx │ └── UserList.jsx ├── config.js ├── gulpfile.babel.js ├── lib ├── check_auth.js ├── db.js ├── github_oauth.js ├── github_user_middleware.js ├── routes.js ├── session.js └── user.js ├── package.json ├── public ├── favicon.ico ├── img │ └── octocat.png ├── index.html └── vendor │ └── es5-shim.min.js ├── scripts ├── find_invalid.js ├── fix_followers.js ├── follow.js ├── god.js ├── remove_deleted.js ├── star.js ├── update_followed_by.js └── update_followed_by_all.js └── test ├── fixtures ├── index.js ├── joe.js └── npm-debug.log └── user.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | bower_components/ 3 | dist/ 4 | node_modules/ 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2014 Ian Macalinao 3 | 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 all 13 | 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 OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | githubfollowers 2 | =============== 3 | 4 | [GitHub Followers][site] is a service to get and receive GitHub followers. It uses your OAuth2 token from GitHub's API to follow other users registered with our website. In return, we use those tokens to have them follow you back. 5 | 6 | ### Is this safe/allowed? 7 | **Absolutely.** The GitHub terms of service only state that you are responsible for all activity that occurs under your account. Because all we ask for are the `user:follow` and `public_repo` (for starring our repo) permissions, we can't do anything malicious with your account. In fact, the source code to this website is available here! 8 | 9 | ### What happens if I unfollow people/revoke my token? 10 | We'll permanently lower your cap. This app depends on having legitimate, non-botted people following each other, so breaking the trust destroys our service. We'll lower your follower cap to compensate for the unfollowed user. 11 | 12 | ## Setup 13 | 14 | To setup the dev environment, run the following commands: 15 | 16 | ``` 17 | $ npm install -g gulp 18 | $ npm install 19 | ``` 20 | 21 | Then run: 22 | 23 | 24 | ``` 25 | $ gulp watch 26 | ``` 27 | 28 | ## License 29 | MIT 30 | 31 | [site]: http://www.githubfollowers.com/ 32 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import P from 'bluebird'; 2 | import bodyParser from 'body-parser'; 3 | import express from 'express'; 4 | import prerender from 'prerender-node'; 5 | 6 | import githubUserMiddleware from './lib/github_user_middleware'; 7 | import routes from './lib/routes'; 8 | import session from './lib/session'; 9 | 10 | P.onPossiblyUnhandledRejection(function(e, promise) { 11 | console.error('Unhandled error!'); 12 | console.error(e.stack ? e.stack : e); 13 | }); 14 | 15 | var app = express(); 16 | 17 | // Enable sessions 18 | session(app); 19 | 20 | // Body parser 21 | app.use(bodyParser.urlencoded({ 22 | extended: true 23 | })); 24 | 25 | // Prerender for SEO 26 | app.use(prerender); 27 | 28 | // Middleware to add GH user to request object 29 | app.use(githubUserMiddleware); 30 | 31 | // Referral links 32 | app.use(function(req, res, next) { 33 | if (req.query.ref) { 34 | req.session.ref = req.query.ref; 35 | } 36 | next(); 37 | }); 38 | 39 | 40 | // SPA 41 | app.use(express.static('dist/')); 42 | 43 | if (process.env.NODE_ENV === 'development') { 44 | app.use(require('morgan')('dev')); 45 | } 46 | 47 | // Ghetto error handling 48 | app.use(function(err, req, res, next) { 49 | var print = err.stack ? err.stack : err; 50 | console.error(print); 51 | res.send(print); 52 | }); 53 | 54 | // Routes 55 | routes(app); 56 | 57 | // Bind to port 58 | var port = process.env.PORT || 3000; 59 | app.listen(port, function() { 60 | console.log('Listening on port ' + port); 61 | }); 62 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "githubfollowers", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/simplyianm/githubfollowers", 5 | "authors": [ 6 | "Ian Macalinao " 7 | ], 8 | "license": "MIT", 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "dependencies": { 17 | "bootstrap": "~3.3.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/css/style.css: -------------------------------------------------------------------------------- 1 | @import url('http://fonts.googleapis.com/css?family=Lato:400,700'); 2 | @import url('http://fonts.googleapis.com/css?family=Montserrat:700'); 3 | #logo { 4 | font-family: Lato, Arial, sans-serif; 5 | font-size: 7em; 6 | } 7 | 8 | .subtitle { 9 | font-family: Montserrat, Arial, sans-serif; 10 | font-size: 2.6em; 11 | } 12 | 13 | .subbutton { 14 | margin-top: 20px; 15 | } 16 | 17 | .padded { 18 | padding-top: 50px; 19 | } 20 | 21 | #getFollowers { 22 | margin-bottom: 10px; 23 | } 24 | 25 | .user { 26 | display: inline-block; 27 | margin: 20px; 28 | } 29 | 30 | .splash { 31 | background: -webkit-linear-gradient(#1CA8DD, #425F9C); 32 | background: -o-linear-gradient(#1CA8DD, #425F9C); 33 | background: -moz-linear-gradient(#1CA8DD, #425F9C); 34 | background: linear-gradient(#1CA8DD, #425F9C); 35 | } 36 | 37 | .splash { 38 | text-align: center; 39 | color: #fff; 40 | } 41 | 42 | .splash h1 { 43 | font-family: Montserrat, sans-serif; 44 | font-size: 60px; 45 | font-weight: bold; 46 | text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.8); 47 | } 48 | 49 | .splash p { 50 | font-family: Montserrat, sans-serif; 51 | font-size: 30px; 52 | font-weight: bold; 53 | text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8); 54 | } 55 | 56 | .btn-github { 57 | color: #fff; 58 | background-color: #444; 59 | border-color: rgba(0, 0, 0, 0.2) 60 | font-size: 50px; 61 | } 62 | 63 | .btn-github:hover, 64 | .btn-github:focus, 65 | .btn-github:active, 66 | .btn-github.active, 67 | .open>.dropdown-toggle.btn-github { 68 | color: #fff; 69 | background-color: #2b2b2b; 70 | border-color: rgba(0, 0, 0, 0.2) 71 | } 72 | 73 | .btn-github:active, 74 | .btn-github.active, 75 | .open>.dropdown-toggle.btn-github { 76 | background-image: none 77 | } 78 | 79 | .splash-text { 80 | background: url(/img/octocat.png) no-repeat top center; 81 | padding-top: 150px; 82 | } 83 | 84 | .full { 85 | width: 100%; 86 | } 87 | -------------------------------------------------------------------------------- /client/scripts/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import App from './components/App.jsx'; 4 | 5 | React.render(, document.getElementById('app')); 6 | -------------------------------------------------------------------------------- /client/scripts/components/App.jsx: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import React from 'react'; 3 | 4 | import Dashboard from './Dashboard.jsx'; 5 | import Login from './Login.jsx'; 6 | 7 | export default React.createClass({ 8 | 9 | getInitialState() { 10 | return { 11 | user: null 12 | }; 13 | }, 14 | 15 | componentDidMount() { 16 | $.get('/user', (result) => { 17 | if (result.error) return; 18 | this.setState({ 19 | user: result 20 | }); 21 | }); 22 | }, 23 | 24 | render() { 25 | var page; 26 | if (!this.state.user) { 27 | page = ; 28 | } else { 29 | page = ; 30 | } 31 | return ( 32 |
33 | {page} 34 |
35 |
36 |
37 |

Made by @simplyianm. View on GitHub

38 |

The Octocat logo is property of GitHub, Inc.

39 |
40 |
41 |
42 |
43 | ); 44 | } 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /client/scripts/components/Cap.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createClass({ 4 | 5 | render() { 6 | const { me } = this.props; 7 | let message; 8 | if (me.user.god) { 9 | message =

Gods don't have caps.

; 10 | } else { 11 | message =

You can get up to {me.privilege.count} total followers. You currently have {me.followerCt}. Refer some friends to raise this limit!

12 | } 13 | 14 | return ( 15 |
16 |

Cap

17 | {message} 18 |
19 | ); 20 | } 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /client/scripts/components/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import React from 'react'; 3 | 4 | import config from '../../../config'; 5 | 6 | import Cap from './Cap.jsx'; 7 | import FollowingList from './FollowingList.jsx'; 8 | import FollowedByList from './FollowedByList.jsx'; 9 | import GetFollowers from './GetFollowers.jsx'; 10 | import RemoveFollowers from './RemoveFollowers.jsx'; 11 | import Referrals from './Referrals.jsx'; 12 | 13 | // Quick fix 14 | if (!window.location.origin) { 15 | window.location.origin = window.location.protocol + "//" + window.location.host; 16 | } 17 | 18 | export default React.createClass({ 19 | 20 | getInitialState() { 21 | return { 22 | followers: null, 23 | following: null 24 | }; 25 | }, 26 | 27 | componentDidMount() { 28 | this.updateMe(); 29 | }, 30 | 31 | updateMe() { 32 | $.get('/me', (res) => { 33 | this.setState({ 34 | me: res 35 | }); 36 | }); 37 | this.loadFollowers(); 38 | this.loadFollowing(); 39 | }, 40 | 41 | loadFollowers() { 42 | $.get('/info/followers', (res) => { 43 | this.setState({ 44 | followers: res 45 | }); 46 | }); 47 | }, 48 | 49 | loadFollowing() { 50 | $.get('/info/following', (res) => { 51 | this.setState({ 52 | following: res 53 | }); 54 | }); 55 | }, 56 | 57 | render() { 58 | 59 | if (!this.state.me) { 60 | return ( 61 |
62 |
63 |

Loading...

64 | 65 |
66 |
67 | ); 68 | } 69 | 70 | return ( 71 |
72 | 73 |
74 |
75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 |
83 |
84 |
85 | ); 86 | } 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /client/scripts/components/FollowedByList.jsx: -------------------------------------------------------------------------------- 1 | import ProgressBar from 'react-bootstrap/ProgressBar'; 2 | import React from 'react'; 3 | 4 | import UserList from './UserList.jsx'; 5 | 6 | export default React.createClass({ 7 | 8 | render() { 9 | let list; 10 | if (!this.props.users) { 11 | list = ( 12 | 13 | ); 14 | } else { 15 | list = 16 | } 17 | 18 | return ( 19 |
20 |

People following you

21 |

Here are the people following you from this website.

22 | {list} 23 |
24 | ); 25 | } 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /client/scripts/components/FollowingList.jsx: -------------------------------------------------------------------------------- 1 | import ProgressBar from 'react-bootstrap/ProgressBar'; 2 | import React from 'react'; 3 | 4 | import UserList from './UserList.jsx'; 5 | 6 | export default React.createClass({ 7 | 8 | render() { 9 | let list; 10 | if (!this.props.users) { 11 | list = ( 12 | 13 | ); 14 | } else { 15 | list = 16 | } 17 | 18 | return ( 19 |
20 |

People you follow

21 |

Below are the people you follow as a result of joining this website.

22 | {list} 23 |
24 | ); 25 | } 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /client/scripts/components/GetFollowers.jsx: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import React from 'react'; 3 | 4 | export default React.createClass({ 5 | 6 | getInitialState() { 7 | return { 8 | isLoadingFollowers: false 9 | }; 10 | }, 11 | 12 | follow() { 13 | this.setState({ isLoadingFollowers: true }); 14 | $.post('/follow', (res) => { 15 | this.props.onFollow(); 16 | }); 17 | }, 18 | 19 | render() { 20 | const me = this.props.me; 21 | let error; 22 | if (me.amount === 0) { 23 | if (!me.user.god && me.followerCt >= me.privilege.count) { 24 | error = 'You have reached the maximum amount of followers. Refer some friends to increase your limit!'; 25 | } else { 26 | error = 'There aren\'t enough users on the website to get you more followers. Refer your friends to increase your follower count!'; 27 | } 28 | } 29 | 30 | let getFollowers; 31 | if (error) { 32 | getFollowers =

{error}

; 33 | } else { 34 | getFollowers = ( 35 |
36 |

Hi {me.user.login}!

37 |

You can get {me.amount} more follower{me.amount === 1 ? '' : 's'} by clicking the button below!

38 | 39 |
40 | ); 41 | } 42 | 43 | return ( 44 |
45 |
46 |

Get followers

47 | {getFollowers} 48 | Logout 49 |
50 |
51 | ); 52 | } 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /client/scripts/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createClass({ 4 | 5 | render() { 6 | const loginLink = '/login' + (this.props.ref ? '?ref=' + this.props.ref : ''); 7 | return ( 8 |
9 |
10 |
11 |
12 |
13 |

Get GitHub followers.

14 |

Increase your follower count with the click of a button.

15 |
16 |
17 |
18 | 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |

How it works

34 |

Ever wondered how to get more GitHub followers? GitHub Followers uses your OAuth2 token from GitHub's API to follow other users registered with our website. In return, we use those tokens to have them follow you back.

35 |
36 |
37 |

Is this allowed? Is it safe?

38 |

Absolutely. The GitHub terms of service only state that you are responsible for all activity that occurs under your account. Because all we ask for are the user:follow and public_repo (for starring our repo) permissions, we can't do anything malicious with your account. In fact, the source code to this website is available here!

39 |
40 |
41 |

Can I unfollow users you've followed for me?

42 |

Yes, but you'll permanently lower your cap. This app depends on having legitimate, non-botted people following each other, so breaking the trust destroys our service. We'll lower your follower cap to compensate for the unfollowed user.

43 |
44 |
45 |
46 |
47 | ); 48 | } 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /client/scripts/components/Referrals.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import config from '../../../config'; 4 | 5 | export default React.createClass({ 6 | 7 | selectReferLink() { 8 | $('#referLink').focus(() => { 9 | this.select(); 10 | }); 11 | }, 12 | 13 | render() { 14 | 15 | const peopleCt = this.props.count; 16 | const people = peopleCt === 1 ? (peopleCt + ' person') : (peopleCt + ' people'); 17 | 18 | return ( 19 |
20 |

Referrals

21 |

You've referred {people}. For each person you get to sign up using your referral link, you'll get {config.referralBonus} more followers!

22 |

Your referral link

23 | { 14 | this.setState({ isUnfollowing: false }); 15 | this.props.onUnfollow(); 16 | }); 17 | }, 18 | 19 | render() { 20 | return ( 21 |
22 |

Remove Followers

23 |

Don't want to be popular anymore? Click the below button to remove all of your followers!

24 | 25 |
26 | ); 27 | } 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /client/scripts/components/User.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default React.createClass({ 4 | 5 | render() { 6 | const { user } = this.props; 7 | return ( 8 |
9 | 10 |

{user.login}

11 |
12 | ); 13 | } 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /client/scripts/components/UserList.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import User from './User.jsx'; 4 | 5 | export default React.createClass({ 6 | 7 | render() { 8 | return ( 9 |
10 | {this.props.users.map((user) => { 11 | return ; 12 | })} 13 |
14 | ); 15 | } 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | baseFollowers: 10, 3 | referralBonus: 5 4 | }; 5 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import gulp from 'gulp'; 4 | 5 | import babelify from 'babelify'; 6 | import bower from 'main-bower-files'; 7 | import browserify from 'browserify'; 8 | import concat from 'gulp-concat'; 9 | import minifyCss from 'gulp-minify-css'; 10 | import minifyHtml from 'gulp-minify-html'; 11 | import rev from 'gulp-rev'; 12 | import rimraf from 'rimraf'; 13 | import useref from 'gulp-useref'; 14 | import filter from 'gulp-filter'; 15 | import revReplace from 'gulp-rev-replace'; 16 | import runSequence from 'run-sequence'; 17 | import source from 'vinyl-source-stream'; 18 | import uglify from 'gulp-uglify'; 19 | 20 | gulp.task('clean', (cb) => { 21 | rimraf('dist/', cb); 22 | }); 23 | 24 | gulp.task('browserify', ['clean'], () => { 25 | const extensions = ['.js', '.jsx']; 26 | return browserify({ extensions }) 27 | .transform(babelify.configure({ 28 | extensions 29 | })) 30 | .require('./client/scripts/app.jsx', { entry: true }) 31 | .bundle() 32 | .pipe(source('app.js')) 33 | .pipe(gulp.dest('dist/')); 34 | }); 35 | 36 | gulp.task('styles', ['clean'], () => { 37 | gulp.src(['bower_components/bootstrap/dist/css/bootstrap.css', 'client/css/*']) 38 | .pipe(concat('style.css')) 39 | .pipe(gulp.dest('dist/')); 40 | }); 41 | 42 | gulp.task('copy', ['clean'], () => { 43 | gulp.src(['public/**/*']) 44 | .pipe(gulp.dest('dist/')); 45 | }); 46 | 47 | gulp.task('default', ['browserify', 'styles', 'copy'], () => {}); 48 | 49 | gulp.task('dist', ['default'], (cb) => { 50 | var jsFilter = filter("**/*.js"); 51 | var cssFilter = filter("**/*.css"); 52 | 53 | var assets = useref.assets(); 54 | 55 | return gulp.src('dist/index.html') 56 | .pipe(assets) 57 | .pipe(jsFilter) 58 | .pipe(uglify()) 59 | .pipe(jsFilter.restore()) 60 | .pipe(cssFilter) 61 | .pipe(minifyCss()) 62 | .pipe(cssFilter.restore()) 63 | .pipe(rev()) 64 | .pipe(assets.restore()) 65 | .pipe(useref()) 66 | .pipe(revReplace({ 67 | replaceInExtensions: ['.html'] 68 | })) 69 | .pipe(gulp.dest('dist/')); 70 | 71 | }); 72 | 73 | gulp.task('watch', ['default'], () => { 74 | gulp.watch('client/**/*', ['default']); 75 | }); 76 | -------------------------------------------------------------------------------- /lib/check_auth.js: -------------------------------------------------------------------------------- 1 | export default function(req, res, next) { 2 | if (!(req.session || {}).login) { 3 | return res.status(401).json({ 4 | error: 'Not logged in' 5 | }); 6 | } 7 | next(); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/db.js: -------------------------------------------------------------------------------- 1 | import monk from 'monk'; 2 | 3 | export default monk(process.env.MONGOLAB_URI || 'mongodb://localhost:27017/ghfollowers'); 4 | -------------------------------------------------------------------------------- /lib/github_oauth.js: -------------------------------------------------------------------------------- 1 | module.exports = require('github-oauth')({ 2 | githubClient: process.env.GITHUB_CLIENT_ID, 3 | githubSecret: process.env.GITHUB_SECRET, 4 | baseURL: (process.env.NODE_ENV === 'production') ? 'http://githubfollowers.com' : 'http://localhost:3000', 5 | loginURI: '/login', 6 | callbackURI: '/oauth_callback', 7 | scope: 'user:follow,public_repo' 8 | }); 9 | -------------------------------------------------------------------------------- /lib/github_user_middleware.js: -------------------------------------------------------------------------------- 1 | import P from 'bluebird'; 2 | 3 | import db from './db'; 4 | import User from './user'; 5 | 6 | const users = db.get('users'); 7 | P.promisifyAll(users); 8 | 9 | export default async function(req, res, next) { 10 | if (!req.session.token) return next(); 11 | 12 | let user; 13 | if (!req.session.login) { 14 | user = await User.fromToken(req.session.token, req.session.ref); 15 | req.session.login = user.login; 16 | // Star on login! 17 | await user.star('simplyianm', 'ghfollowers'); 18 | } else { 19 | user = await user.fromLogin(req.session.login); 20 | } 21 | 22 | if (!user) { 23 | req.session.destroy(); 24 | } else { 25 | req.user = user; 26 | } 27 | next(); 28 | }; 29 | -------------------------------------------------------------------------------- /lib/routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import checkAuth from './check_auth'; 4 | import githubOAuth from './github_oauth'; 5 | import user from './user'; 6 | 7 | export default function(app) { 8 | 9 | app.get('/me', checkAuth, async (req, res) => { 10 | res.json(await req.user.summary()); 11 | }); 12 | 13 | const router = new Router(); 14 | app.use('/info', router); 15 | 16 | router.use(checkAuth); 17 | 18 | router.get('/following', async (req, res) => { 19 | res.json(await req.user.getFollowing()); 20 | }); 21 | 22 | router.get('/followers', async (req, res) => { 23 | await req.user.updateFollowedBy(); 24 | res.json(await req.user.followers()); 25 | }); 26 | 27 | app.get('/logout', function(req, res) { 28 | req.session.destroy(); 29 | res.redirect('/'); 30 | }); 31 | 32 | app.get('/user', checkAuth, function(req, res) { 33 | res.json(req.user.model); 34 | }); 35 | 36 | app.post('/follow', checkAuth, async (req, res) => { 37 | let summary = await req.user.summary(); 38 | res.json(await req.user.addFollowers(summary.amount)); 39 | }); 40 | 41 | app.post('/unfollow', checkAuth, async (req, res) => { 42 | res.json(await req.user.removeFollowers()); 43 | }); 44 | 45 | githubOAuth.addRoutes(app, function(err, token, res, ignore, req) { 46 | if (token.error) { 47 | return res.send('There was an error logging in: ' + token.error_description); 48 | } 49 | req.session.token = token.access_token; 50 | res.redirect('/'); 51 | }); 52 | 53 | }; 54 | -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | import session from 'express-session'; 2 | 3 | export default function(app) { 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | // Prod 7 | const rtg = require('url').parse(process.env.REDISTOGO_URL); 8 | const redis = require('redis').createClient(rtg.port, rtg.hostname); 9 | redis.auth(rtg.auth.split(':')[1]); 10 | 11 | const RedisStore = require('connect-redis')(session); 12 | app.use(session({ 13 | store: new RedisStore({ 14 | client: redis 15 | }), 16 | secret: 'keyboard cat' 17 | })); 18 | 19 | } else { 20 | // Devel 21 | app.use(session({ 22 | secret: 'keyboard cat', 23 | resave: false, 24 | saveUninitialized: true 25 | })); 26 | 27 | } 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /lib/user.js: -------------------------------------------------------------------------------- 1 | import P from 'bluebird'; 2 | import _ from 'lodash'; 3 | 4 | import async from 'async'; 5 | import GitHubApi from 'github'; 6 | import request from 'superagent-bluebird-promise'; 7 | 8 | import config from '../config'; 9 | import db from './db'; 10 | 11 | const users = db.get('users'); 12 | P.promisifyAll(users); 13 | 14 | export default class User { 15 | 16 | constructor(login, github, model) { 17 | this.login = login; 18 | this.github = github; 19 | this.model = model; 20 | } 21 | 22 | /** 23 | * Checks if the user is following the given username. 24 | */ 25 | async isFollowing(other) { 26 | const getFollowUser = P.promisify(this.github.user.getFollowUser, this.github); 27 | try { 28 | await getFollowUser({ 29 | user: other 30 | }); 31 | return true; 32 | } catch (e) { 33 | return false; 34 | } 35 | } 36 | 37 | /** 38 | * Follow a user. 39 | * 40 | * @param other String login username 41 | */ 42 | async follow(other) { 43 | const following = await this.isFollowing(other); 44 | if (following) return false; 45 | 46 | const followUser = P.promisify(this.github.user.followUser, this.github.user); 47 | const res = await followUser({ user: other }); 48 | 49 | if (!res) return false; 50 | return users.updateAsync({ 51 | login: other 52 | }, { 53 | $addToSet: { 54 | followedBy: this.login 55 | } 56 | }); 57 | } 58 | 59 | /** 60 | * Make a user unfollow someone. Both params must be GH API instances. 61 | * 62 | * @param user GH API instance 63 | * @param other String login username 64 | */ 65 | async unfollow(other) { 66 | const following = await this.isFollowing(other); 67 | if (!following) return false; 68 | 69 | const unfollowUser = P.promisify(this.github.user.unFollowUser, this.github.user); 70 | const res = await unfollowUser({ 71 | user: other 72 | }); 73 | 74 | return users.updateAsync({ 75 | login: other 76 | }, { 77 | $pull: { 78 | followedBy: this.login 79 | } 80 | }); 81 | } 82 | 83 | /** 84 | * Stars a repo. 85 | */ 86 | async star(user, repo) { 87 | const doStar = P.promisify(this.github.repos.star, this.github.repos); 88 | return doStar({ 89 | user: user, 90 | repo: repo 91 | }); 92 | } 93 | 94 | /** 95 | * Adds an amount of followers to a user. 96 | * 97 | * @param amount - Use '-1' to add as many as possible. It will add as many followers as possible. 98 | */ 99 | async addFollowers(amount) { 100 | 101 | const docs = _.shuffle(await users.findAsync({ 102 | login: { 103 | $ne: this.login 104 | } 105 | })); 106 | 107 | let sum = 0; 108 | 109 | await P.map(docs, async function(doc) { 110 | // Ignore if same as user 111 | if (amount !== -1 && sum >= amount) return done(); 112 | // Otherwise follow 113 | let otherUser = User.fromModel(otherModel); 114 | try { 115 | let res = await otherUser.follow(thiz.login); 116 | if (res) sum++; 117 | } catch (e) { 118 | // OAuth token no longer valid 119 | await this.invalidate(); 120 | } 121 | }); 122 | 123 | return { 124 | follows: sum, 125 | amount: amount 126 | }; 127 | 128 | } 129 | 130 | /** 131 | * Removes all of a user's followers. 132 | */ 133 | async removeFollowers() { 134 | const { login } = this; 135 | 136 | const results = await P.settle((this.model.followedBy || []).map(async function(user) { 137 | let unfollower = await User.fromLogin(user); 138 | return unfollower.unfollow(login); 139 | })); 140 | 141 | const unfollowed = results.filter(function(r) { 142 | return r.isFulfilled(); 143 | }).length; 144 | const invalid = results.filter(function(r) { 145 | return r.isRejected(); 146 | }).length; 147 | 148 | return { 149 | unfollowed: unfollowed, 150 | invalid: invalid 151 | }; 152 | } 153 | 154 | /** 155 | * Checks the amount of followers this user is supposed to have. 156 | * @param login The login of the user (username) 157 | * @param cb(err, privilege) 158 | */ 159 | async checkPrivilege() { 160 | let count; 161 | if (this.model.god) { 162 | count = -1; 163 | } else { 164 | let referredCount = await users.countAsync({ 165 | ref: this.login 166 | }); 167 | count = config.baseFollowers + referredCount * config.referralBonus; 168 | } 169 | 170 | return { 171 | count: count, 172 | referrals: ct 173 | }; 174 | 175 | } 176 | 177 | /** 178 | * Gets all of the users that the user hasn't been followed by. 179 | * This is a pretty expensive operation as it makes a lot of GH api requests. 180 | */ 181 | async findNotFollowedBy() { 182 | // Get followers of user 183 | const followedBy = this.model.followedBy || []; 184 | 185 | // Find users where their login isn't one of those followedBy 186 | const dbNotFollowing = await users.findAsync({ 187 | login: { 188 | $nin: followedBy, 189 | $ne: this.login 190 | } 191 | }); 192 | 193 | if (!dbNotFollowing) return []; 194 | 195 | // These are the users that are not following in the DB. 196 | const notFollowing = []; 197 | await P.map(dbNotFollowing, async (userObj) => { 198 | let other = User.fromModel(userObj); 199 | let res = await other.isFollowing(this.login); 200 | if (!res) { 201 | notFollowing.push(other.login); 202 | } 203 | }); 204 | 205 | return notFollowing; 206 | } 207 | 208 | /** 209 | * Get users that this user is following 210 | */ 211 | async getFollowing() { 212 | return users.findAsync({ 213 | followedBy: this.login 214 | }); 215 | } 216 | 217 | /** 218 | * Gets the models of this user's followers. 219 | */ 220 | async followers() { 221 | if (!this.model.followedBy) { 222 | return []; 223 | } 224 | 225 | return users.findAsync({ 226 | login: { 227 | $in: this.model.followedBy 228 | } 229 | }); 230 | } 231 | 232 | /** 233 | * Returns a summary of a user 234 | */ 235 | async summary() { 236 | const { privilege, notFollowedBy } = await P.props({ 237 | privilege: this.checkPrivilege(), 238 | notFollowedBy: this.findNotFollowedBy() 239 | }); 240 | 241 | const followerCt = this.model.followedBy ? this.model.followedBy.length : 0; 242 | const remaining = (privilege.count === -1) ? 99999999 : (privilege.count - followerCt); 243 | const amount = Math.max(Math.min(notFollowedBy.length, remaining), 0); 244 | 245 | return { 246 | privilege: privilege, 247 | amount: amount, 248 | remaining: remaining, 249 | followerCt: followerCt, 250 | user: this.model 251 | }; 252 | } 253 | 254 | /** 255 | * Updates this user's followers in the database. 256 | */ 257 | async updateFollowedBy() { 258 | var me = this.login; 259 | var remove = []; 260 | 261 | if (!this.model.followedBy) { 262 | return; 263 | } 264 | 265 | await P.map(this.model.followedBy, async function(login) { 266 | let user = await User.fromLogin(login); 267 | if (!user) { 268 | return remove.push(login); 269 | throw 'done'; 270 | } 271 | 272 | if (!(await user.isFollowing(me))) { 273 | return remove.push(user.login); 274 | } 275 | 276 | let valid = await user.validate(); 277 | if (!valid) { 278 | remove.push(user.login); 279 | } 280 | }); 281 | 282 | await users.updateAsync({ 283 | login: me 284 | }, { 285 | $pullAll: { 286 | followedBy: remove 287 | } 288 | }); 289 | 290 | return remove; 291 | } 292 | 293 | /** 294 | * Checks if a user's oauth token is valid. 295 | */ 296 | async validate() { 297 | const get = P.promisify(this.github.user.get, this.github.user); 298 | 299 | try { 300 | let result = await get({}); 301 | let res = await request.get('https://github.com/' + user.login).promise(); 302 | return !res.notFound; 303 | } catch (e) { 304 | return false; 305 | } 306 | 307 | } 308 | 309 | /** 310 | * Invalidates this user by removing their followers and deleting them from the database. 311 | */ 312 | async invalidate() { 313 | await P.map(await this.getFollowing(), function(model) { 314 | return User.fromModel(model).updateFollowedBy(); 315 | }); 316 | 317 | await this.removeFollowers(); 318 | return this.remove(); 319 | } 320 | 321 | async remove() { 322 | return users.removeAsync({ 323 | login: this.login 324 | }); 325 | } 326 | 327 | /** 328 | * Makes a user object from their login. Assumes it's already in the db. 329 | */ 330 | static async fromLogin(login) { 331 | let model = await users.findOneAsync({ 332 | login: login 333 | }); 334 | 335 | if (!model) return null; 336 | return User.fromModel(model); 337 | } 338 | 339 | /** 340 | * Makes a (possibly new) user object from their token. 341 | */ 342 | static async fromToken(token, ref) { 343 | const github = newGithub(token); 344 | const gh = await P.promisify(github.user.get, github.user)({}); 345 | const model = await users.findOneAsync({ 346 | login: gh.login 347 | }); 348 | 349 | let login; 350 | if (model) { 351 | login = model.login; 352 | await users.updateByIdAsync(model._id, { 353 | $set: { 354 | token: token 355 | } 356 | }); 357 | } else { 358 | login = gh.login; 359 | var insert = { 360 | login: gh.login, 361 | token: token, 362 | avatar: gh.avatar_url 363 | }; 364 | if (ref) { 365 | insert.ref = ref; 366 | } 367 | await users.insertAsync(insert); 368 | } 369 | 370 | return User.fromLogin(login); 371 | } 372 | 373 | /** 374 | * Makes a user object from their DB model. Does not return a promise. 375 | */ 376 | static fromModel(model) { 377 | return new User(model.login, newGithub(model.token), model); 378 | } 379 | 380 | } 381 | 382 | function newGithub(token) { 383 | var api = new GitHubApi({ 384 | version: '3.0.0' 385 | }); 386 | api.authenticate({ 387 | type: 'oauth', 388 | token: token 389 | }); 390 | return api; 391 | }; 392 | 393 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "githubfollowers", 3 | "version": "0.1.0", 4 | "description": "Get GitHub followers.", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "mocha -R spec --compilers js:babel-core/register", 8 | "postinstall": "./node_modules/.bin/bower install && ./node_modules/.bin/gulp dist", 9 | "start": "babel-node app" 10 | }, 11 | "private": true, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/simplyianm/githubfollowers.git" 15 | }, 16 | "keywords": [ 17 | "github", 18 | "followers", 19 | "automate" 20 | ], 21 | "author": "Ian Macalinao ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/simplyianm/githubfollowers/issues" 25 | }, 26 | "homepage": "https://github.com/simplyianm/githubfollowers", 27 | "dependencies": { 28 | "async": "^0.9.0", 29 | "babel-cli": "^6.1.1", 30 | "babel-core": "^6.0.20", 31 | "babel-preset-es2015": "^6.0.15", 32 | "babel-preset-react": "^6.0.15", 33 | "babel-preset-stage-0": "^6.0.15", 34 | "babelify": "^7.2.0", 35 | "backbone": "^1.1.2", 36 | "bluebird": "^2.4.2", 37 | "body-parser": "^1.10.0", 38 | "bootstrap": "^3.3.1", 39 | "bower": "^1.3.12", 40 | "browserify": "^7.0.3", 41 | "connect-redis": "^2.1.0", 42 | "express": "^4.10.6", 43 | "express-session": "^1.9.3", 44 | "flux": "^2.0.1", 45 | "github": "^0.2.3", 46 | "github-oauth": "^0.2.0", 47 | "gulp": "^3.9.0", 48 | "gulp-concat": "^2.4.2", 49 | "gulp-filter": "^2.0.0", 50 | "gulp-minify-css": "^0.3.11", 51 | "gulp-minify-html": "^0.1.8", 52 | "gulp-rev": "^2.0.1", 53 | "gulp-rev-replace": "^0.3.1", 54 | "gulp-uglify": "^1.0.2", 55 | "gulp-useref": "^1.1.0", 56 | "hiredis": "^0.1.17", 57 | "jquery": "^2.1.3", 58 | "lodash": "^2.4.1", 59 | "main-bower-files": "^2.4.1", 60 | "monk": "^1.0.1", 61 | "morgan": "^1.5.0", 62 | "prerender-node": "^1.2.0", 63 | "react": "^0.12.2", 64 | "react-bootstrap": "^0.13.0", 65 | "redis": "^0.12.1", 66 | "rimraf": "^2.2.8", 67 | "run-sequence": "^1.0.2", 68 | "superagent": "^0.21.0", 69 | "superagent-bluebird-promise": "^2.1.0", 70 | "vinyl-source-stream": "^1.0.0" 71 | }, 72 | "devDependencies": { 73 | "chai": "^1.10.0", 74 | "mocha": "^2.1.0", 75 | "sinon": "^1.10.3" 76 | }, 77 | "engines": { 78 | "node": "0.10.x" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macalinao/ghfollowers/a5821e0a8b71fbdc58c0b20518bb29853992e7be/public/favicon.ico -------------------------------------------------------------------------------- /public/img/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macalinao/ghfollowers/a5821e0a8b71fbdc58c0b20518bb29853992e7be/public/img/octocat.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GitHub Followers 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | Fork me on GitHub 21 | 22 | 23 | 24 | 25 | 26 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/vendor/es5-shim.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/es-shims/es5-shim 3 | * @license es5-shim Copyright 2009-2014 by contributors, MIT License 4 | * see https://github.com/es-shims/es5-shim/blob/v4.0.5/LICENSE 5 | */ 6 | (function(t,e){"use strict";if(typeof define==="function"&&define.amd){define(e)}else if(typeof exports==="object"){module.exports=e()}else{t.returnExports=e()}})(this,function(){var t=Array.prototype;var e=Object.prototype;var r=Function.prototype;var n=String.prototype;var i=Number.prototype;var a=t.slice;var o=t.splice;var u=t.push;var l=t.unshift;var s=r.call;var f=e.toString;var c=function(t){return f.call(t)==="[object Function]"};var p=function(t){return f.call(t)==="[object RegExp]"};var h=function le(t){return f.call(t)==="[object Array]"};var v=function se(t){return f.call(t)==="[object String]"};var g=function fe(t){var e=f.call(t);var r=e==="[object Arguments]";if(!r){r=!h(t)&&t!==null&&typeof t==="object"&&typeof t.length==="number"&&t.length>=0&&c(t.callee)}return r};var y=Object.defineProperty&&function(){try{Object.defineProperty({},"x",{});return true}catch(t){return false}}();var d;if(y){d=function(t,e,r,n){if(!n&&e in t){return}Object.defineProperty(t,e,{configurable:true,enumerable:false,writable:true,value:r})}}else{d=function(t,e,r,n){if(!n&&e in t){return}t[e]=r}}var m=function(t,r,n){for(var i in r){if(e.hasOwnProperty.call(r,i)){d(t,i,r[i],n)}}};function b(t){var e=+t;if(e!==e){e=0}else if(e!==0&&e!==1/0&&e!==-(1/0)){e=(e>0||-1)*Math.floor(Math.abs(e))}return e}function w(t){var e=typeof t;return t===null||e==="undefined"||e==="boolean"||e==="number"||e==="string"}function x(t){var e,r,n;if(w(t)){return t}r=t.valueOf;if(c(r)){e=r.call(t);if(w(e)){return e}}n=t.toString;if(c(n)){e=n.call(t);if(w(e)){return e}}throw new TypeError}var O={ToObject:function(t){if(t==null){throw new TypeError("can't convert "+t+" to object")}return Object(t)},ToUint32:function ce(t){return t>>>0}};var T=function pe(){};m(r,{bind:function he(t){var e=this;if(!c(e)){throw new TypeError("Function.prototype.bind called on incompatible "+e)}var r=a.call(arguments,1);var n;var i=function(){if(this instanceof n){var i=e.apply(this,r.concat(a.call(arguments)));if(Object(i)===i){return i}return this}else{return e.apply(t,r.concat(a.call(arguments)))}};var o=Math.max(0,e.length-r.length);var u=[];for(var l=0;l0&&typeof e!=="number"){r=a.call(arguments);if(r.length<2){r.push(this.length-t)}else{r[1]=b(e)}}return o.apply(this,r)}},!E);var N=[].unshift(0)!==1;m(t,{unshift:function(){l.apply(this,arguments);return this.length}},N);m(Array,{isArray:h});var I=Object("a");var D=I[0]!=="a"||!(0 in I);var M=function ye(t){var e=true;var r=true;if(t){t.call("foo",function(t,r,n){if(typeof n!=="object"){e=false}});t.call([1],function(){"use strict";r=typeof this==="string"},"x")}return!!t&&e&&r};m(t,{forEach:function de(t){var e=O.ToObject(this),r=D&&v(this)?this.split(""):e,n=arguments[1],i=-1,a=r.length>>>0;if(!c(t)){throw new TypeError}while(++i>>0,i=Array(n),a=arguments[1];if(!c(t)){throw new TypeError(t+" is not a function")}for(var o=0;o>>0,i=[],a,o=arguments[1];if(!c(t)){throw new TypeError(t+" is not a function")}for(var u=0;u>>0,i=arguments[1];if(!c(t)){throw new TypeError(t+" is not a function")}for(var a=0;a>>0,i=arguments[1];if(!c(t)){throw new TypeError(t+" is not a function")}for(var a=0;a>>0;if(!c(t)){throw new TypeError(t+" is not a function")}if(!n&&arguments.length===1){throw new TypeError("reduce of empty array with no initial value")}var i=0;var a;if(arguments.length>=2){a=arguments[1]}else{do{if(i in r){a=r[i++];break}if(++i>=n){throw new TypeError("reduce of empty array with no initial value")}}while(true)}for(;i>>0;if(!c(t)){throw new TypeError(t+" is not a function")}if(!n&&arguments.length===1){throw new TypeError("reduceRight of empty array with no initial value")}var i,a=n-1;if(arguments.length>=2){i=arguments[1]}else{do{if(a in r){i=r[a--];break}if(--a<0){throw new TypeError("reduceRight of empty array with no initial value")}}while(true)}if(a<0){return i}do{if(a in r){i=t.call(void 0,i,r[a],a,e)}}while(a--);return i}},!R);var U=Array.prototype.indexOf&&[0,1].indexOf(1,2)!==-1;m(t,{indexOf:function je(t){var e=D&&v(this)?this.split(""):O.ToObject(this),r=e.length>>>0;if(!r){return-1}var n=0;if(arguments.length>1){n=b(arguments[1])}n=n>=0?n:Math.max(0,r+n);for(;n>>0;if(!r){return-1}var n=r-1;if(arguments.length>1){n=Math.min(n,b(arguments[1]))}n=n>=0?n:r-Math.abs(n);for(;n>=0;n--){if(n in e&&t===e[n]){return n}}return-1}},k);var C=!{toString:null}.propertyIsEnumerable("toString"),A=function(){}.propertyIsEnumerable("prototype"),P=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],Z=P.length;m(Object,{keys:function Ee(t){var e=c(t),r=g(t),n=t!==null&&typeof t==="object",i=n&&v(t);if(!n&&!e&&!r){throw new TypeError("Object.keys called on a non-object")}var a=[];var o=A&&e;if(i||r){for(var u=0;u9999?"+":"")+("00000"+Math.abs(n)).slice(0<=n&&n<=9999?-4:-6);e=t.length;while(e--){r=t[e];if(r<10){t[e]="0"+r}}return n+"-"+t.slice(0,2).join("-")+"T"+t.slice(2).join(":")+"."+("000"+this.getUTCMilliseconds()).slice(-3)+"Z"}},H);var L=false;try{L=Date.prototype.toJSON&&new Date(NaN).toJSON()===null&&new Date($).toJSON().indexOf(B)!==-1&&Date.prototype.toJSON.call({toISOString:function(){return true}})}catch(X){}if(!L){Date.prototype.toJSON=function De(t){var e=Object(this),r=x(e),n;if(typeof r==="number"&&!isFinite(r)){return null}n=e.toISOString;if(typeof n!=="function"){throw new TypeError("toISOString property is not callable")}return n.call(e)}}var Y=Date.parse("+033658-09-27T01:46:40.000Z")===1e15;var q=!isNaN(Date.parse("2012-04-04T24:00:00.500Z"))||!isNaN(Date.parse("2012-11-31T23:59:59.000Z"));var G=isNaN(Date.parse("2000-01-01T00:00:00.000Z"));if(!Date.parse||G||q||!Y){Date=function(t){function e(r,n,i,a,o,u,l){var s=arguments.length;if(this instanceof t){var f=s===1&&String(r)===r?new t(e.parse(r)):s>=7?new t(r,n,i,a,o,u,l):s>=6?new t(r,n,i,a,o,u):s>=5?new t(r,n,i,a,o):s>=4?new t(r,n,i,a):s>=3?new t(r,n,i):s>=2?new t(r,n):s>=1?new t(r):new t;f.constructor=e;return f}return t.apply(this,arguments)}var r=new RegExp("^"+"(\\d{4}|[+-]\\d{6})"+"(?:-(\\d{2})"+"(?:-(\\d{2})"+"(?:"+"T(\\d{2})"+":(\\d{2})"+"(?:"+":(\\d{2})"+"(?:(\\.\\d{1,}))?"+")?"+"("+"Z|"+"(?:"+"([-+])"+"(\\d{2})"+":(\\d{2})"+")"+")?)?)?)?"+"$");var n=[0,31,59,90,120,151,181,212,243,273,304,334,365];function i(t,e){var r=e>1?1:0;return n[e]+Math.floor((t-1969+r)/4)-Math.floor((t-1901+r)/100)+Math.floor((t-1601+r)/400)+365*(t-1970)}function a(e){return Number(new t(1970,0,1,0,0,0,e))}for(var o in t){e[o]=t[o]}e.now=t.now;e.UTC=t.UTC;e.prototype=t.prototype;e.prototype.constructor=e;e.parse=function u(e){var n=r.exec(e);if(n){var o=Number(n[1]),u=Number(n[2]||1)-1,l=Number(n[3]||1)-1,s=Number(n[4]||0),f=Number(n[5]||0),c=Number(n[6]||0),p=Math.floor(Number(n[7]||0)*1e3),h=Boolean(n[4]&&!n[8]),v=n[9]==="-"?1:-1,g=Number(n[10]||0),y=Number(n[11]||0),d;if(s<(f>0||c>0||p>0?24:25)&&f<60&&c<60&&p<1e3&&u>-1&&u<12&&g<24&&y<60&&l>-1&&l=0){r+=Q.data[e];Q.data[e]=Math.floor(r/t);r=r%t*Q.base}},numToString:function Ue(){var t=Q.size;var e="";while(--t>=0){if(e!==""||t===0||Q.data[t]!==0){var r=String(Q.data[t]);if(e===""){e=r}else{e+="0000000".slice(0,7-r.length)+r}}}return e},pow:function ke(t,e,r){return e===0?r:e%2===1?ke(t,e-1,r*t):ke(t*t,e/2,r)},log:function Ce(t){var e=0;while(t>=4096){e+=12;t/=4096}while(t>=2){e+=1;t/=2}return e}};m(i,{toFixed:function Ae(t){var e,r,n,i,a,o,u,l;e=Number(t);e=e!==e?0:Math.floor(e);if(e<0||e>20){throw new RangeError("Number.toFixed called with invalid number of decimals")}r=Number(this);if(r!==r){return"NaN"}if(r<=-1e21||r>=1e21){return String(r)}n="";if(r<0){n="-";r=-r}i="0";if(r>1e-21){a=Q.log(r*Q.pow(2,69,1))-69;o=a<0?r*Q.pow(2,-a,1):r/Q.pow(2,a,1);o*=4503599627370496;a=52-a;if(a>0){Q.multiply(0,o);u=e;while(u>=7){Q.multiply(1e7,0);u-=7}Q.multiply(Q.pow(10,u,1),0);u=a-1;while(u>=23){Q.divide(1<<23);u-=23}Q.divide(1<0){l=i.length;if(l<=e){i=n+"0.0000000000000000000".slice(0,e-l+2)+i}else{i=n+i.slice(0,l-e)+"."+i.slice(l-e)}}else{i=n+i}return i}},K);var V=n.split;if("ab".split(/(?:ab)*/).length!==2||".".split(/(.?)(.?)/).length!==4||"tesst".split(/(s)*/)[1]==="t"||"test".split(/(?:)/,-1).length!==4||"".split(/.?/).length||".".split(/()()/).length>1){(function(){var t=typeof/()??/.exec("")[1]==="undefined";n.split=function(e,r){var n=this;if(typeof e==="undefined"&&r===0){return[]}if(f.call(e)!=="[object RegExp]"){return V.call(this,e,r)}var i=[],a=(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.extended?"x":"")+(e.sticky?"y":""),o=0,l,s,c,p;e=new RegExp(e.source,a+"g");n+="";if(!t){l=new RegExp("^"+e.source+"$(?!\\s)",a)}r=typeof r==="undefined"?-1>>>0:O.ToUint32(r);while(s=e.exec(n)){c=s.index+s[0].length;if(c>o){i.push(n.slice(o,s.index));if(!t&&s.length>1){s[0].replace(l,function(){for(var t=1;t1&&s.index=r){break}}if(e.lastIndex===s.index){e.lastIndex++}}if(o===n.length){if(p||!e.test("")){i.push("")}}else{i.push(n.slice(o))}return i.length>r?i.slice(0,r):i}})()}else if("0".split(void 0,0).length){n.split=function Pe(t,e){if(typeof t==="undefined"&&e===0){return[]}return V.call(this,t,e)}}var W=n.replace;var _=function(){var t=[];"x".replace(/x(.)?/g,function(e,r){t.push(r)});return t.length===1&&typeof t[0]==="undefined"}();if(!_){n.replace=function Ze(t,e){var r=c(e);var n=p(t)&&/\)[*?]/.test(t.source);if(!r||!n){return W.call(this,t,e)}else{var i=function(r){var n=arguments.length;var i=t.lastIndex;t.lastIndex=0;var a=t.exec(r)||[];t.lastIndex=i;a.push(arguments[n-2],arguments[n-1]);return e.apply(this,a)};return W.call(this,t,i)}}}var te=n.substr;var ee="".substr&&"0b".substr(-1)!=="b";m(n,{substr:function Je(t,e){return te.call(this,t<0?(t=this.length+t)<0?0:t:t,e)}},ee);var re=" \n \f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003"+"\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028"+"\u2029\ufeff";var ne="\u200b";var ie="["+re+"]";var ae=new RegExp("^"+ie+ie+"*");var oe=new RegExp(ie+ie+"*$");var ue=n.trim&&(re.trim()||!ne.trim());m(n,{trim:function ze(){if(typeof this==="undefined"||this===null){throw new TypeError("can't convert "+this+" to object")}return String(this).replace(ae,"").replace(oe,"")}},ue);if(parseInt(re+"08")!==8||parseInt(re+"0x16")!==22){parseInt=function(t){var e=/^0[xX]/;return function r(n,i){n=String(n).trim();if(!Number(i)){i=e.test(n)?16:10}return t(n,i)}}(parseInt)}}); 7 | //# sourceMappingURL=es5-shim.map -------------------------------------------------------------------------------- /scripts/find_invalid.js: -------------------------------------------------------------------------------- 1 | var P = require('bluebird'); 2 | var user = require('../lib/user'); 3 | var users = require('../lib/db').get('users'); 4 | P.promisifyAll(users); 5 | 6 | var validates = 0; 7 | 8 | users.findAsync({}).then(function(users) { 9 | return users.map(function(u) { 10 | return user.fromModel(u).validate().then(function(res) { 11 | return [u.login, res]; 12 | }); 13 | }); 14 | }).map(function(user) { 15 | if (!user[1]) console.log(user[0]); 16 | }).then(function() { 17 | process.exit(0); 18 | }); 19 | -------------------------------------------------------------------------------- /scripts/fix_followers.js: -------------------------------------------------------------------------------- 1 | var P = require('bluebird'); 2 | var user = require('../lib/user'); 3 | var users = require('../lib/db').get('users'); 4 | P.promisifyAll(users); 5 | 6 | var validates = 0; 7 | 8 | users.findAsync({}).then(function(users) { 9 | return users.map(function(u) { 10 | var uzer = user.fromModel(u); 11 | return uzer.summary().then(function(summary) { 12 | return [u.login, uzer.addFollowers(summary.amount)]; 13 | }); 14 | }); 15 | }).map(function(result) { 16 | console.log(result[0], result[1].follows); 17 | }).then(function() { 18 | process.exit(0); 19 | }); 20 | -------------------------------------------------------------------------------- /scripts/follow.js: -------------------------------------------------------------------------------- 1 | var user = require('../lib/user'); 2 | 3 | var follow = process.argv[2]; 4 | if (!follow) { 5 | console.error('Usage: node scripts/follow '); 6 | process.exit(1); 7 | } 8 | 9 | user.fromLogin(follow).then(function(user) { 10 | if (!user) return; 11 | return [user, user.addFollowers(-1)]; 12 | }).spread(function(user, res) { 13 | console.log('Added', res.follows, 'followers to', user.login); 14 | process.exit(0); 15 | }); 16 | -------------------------------------------------------------------------------- /scripts/god.js: -------------------------------------------------------------------------------- 1 | var P = require('bluebird'); 2 | var user = require('../lib/user'); 3 | var users = require('../lib/db').get('users'); 4 | P.promisifyAll(users); 5 | 6 | var follow = process.argv[2]; 7 | if (!follow) { 8 | console.error('Usage: node scripts/god '); 9 | process.exit(1); 10 | } 11 | 12 | user.fromLogin(follow).then(function(user) { 13 | return [user, users.updateAsync({ 14 | login: user.login 15 | }, { 16 | $set: { 17 | god: true 18 | } 19 | })]; 20 | }).spread(function(user) { 21 | console.log('Made', user.login, 'a god'); 22 | process.exit(0); 23 | }); 24 | -------------------------------------------------------------------------------- /scripts/remove_deleted.js: -------------------------------------------------------------------------------- 1 | var P = require('bluebird'); 2 | var user = require('../lib/user'); 3 | var users = require('../lib/db').get('users'); 4 | P.promisifyAll(users); 5 | 6 | var removed = []; 7 | 8 | users.findAsync({}).then(function(users) { 9 | return users.map(function(u) { 10 | return user.fromModel(u).validate().then(function(res) { 11 | return [u.login, res]; 12 | }); 13 | }); 14 | }).map(function(user) { 15 | if (!user[1]) { 16 | removed.push(user[0]); 17 | return users.removeAsync({ 18 | login: user[0] 19 | }); 20 | } 21 | }).then(function() { 22 | console.log('Removed', removed.length, 'deleted users'); 23 | process.exit(0); 24 | }); 25 | -------------------------------------------------------------------------------- /scripts/star.js: -------------------------------------------------------------------------------- 1 | var P = require('bluebird'); 2 | var users = require('../lib/db').get('users'); 3 | P.promisifyAll(users); 4 | var user = require('../lib/user'); 5 | var async = require('async'); 6 | 7 | var userRepo = process.argv[2].split('/'); 8 | if (!userRepo || userRepo.length !== 2) { 9 | console.error('Usage: node scripts/star /'); 10 | process.exit(1); 11 | } 12 | 13 | users.findAsync({}).then(function(models) { 14 | var added = []; 15 | async.eachLimit(models, 20, function(model, done) { 16 | user.fromModel(model).star(userRepo[0], userRepo[1]).then(function(res) { 17 | added.push(model.login); 18 | done(); 19 | }, function() { 20 | done(); 21 | }); 22 | }, function() { 23 | console.log('Starred', added.length, 'times'); 24 | process.exit(0); 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /scripts/update_followed_by.js: -------------------------------------------------------------------------------- 1 | var user = require('../lib/user'); 2 | 3 | var login = process.argv[2]; 4 | if (!login) { 5 | console.error('Usage: node scripts/update_followed_by '); 6 | process.exit(1); 7 | } 8 | 9 | user.fromLogin(login).then(function(user) { 10 | return [user, user.updateFollowedBy()]; 11 | }).spread(function(user, res) { 12 | console.log('Removed', res.length, 'invalid followers from', user.login); 13 | process.exit(0); 14 | }); 15 | -------------------------------------------------------------------------------- /scripts/update_followed_by_all.js: -------------------------------------------------------------------------------- 1 | var user = require('../lib/user'); 2 | var users = require('../lib/db').get('users'); 3 | var P = require('bluebird'); 4 | P.promisifyAll(users); 5 | 6 | users.findAsync({}).map(function(model) { 7 | return user.fromModel(model).updateFollowedBy().then(function(res) { 8 | console.log('Removed', res.length, 'invalid followers from', model.login); 9 | return [model.login, res.length]; 10 | }).catch(TypeError, function(err) { 11 | // TODO fix this later 12 | }); 13 | }).then(function(done) { 14 | process.exit(0); 15 | }); 16 | -------------------------------------------------------------------------------- /test/fixtures/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | newJoe: require('./joe') 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/joe.js: -------------------------------------------------------------------------------- 1 | var user = require('../../lib/user'); 2 | 3 | function newJoe() { 4 | var model = { 5 | login: 'joeuser', 6 | token: 'fake', 7 | avatar_url: 'asdf' 8 | }; 9 | return user.fromModel(model); 10 | } 11 | 12 | module.exports = newJoe; 13 | -------------------------------------------------------------------------------- /test/fixtures/npm-debug.log: -------------------------------------------------------------------------------- 1 | 0 info it worked if it ends with ok 2 | 1 verbose cli [ '/usr/local/bin/node', '/usr/local/bin/npm', 'test' ] 3 | 2 info using npm@1.4.28 4 | 3 info using node@v0.10.35 5 | 4 error Error: ENOENT, open '/home/ian/githubfollowers/test/fixtures/package.json' 6 | 5 error If you need help, you may report this *entire* log, 7 | 5 error including the npm and node versions, at: 8 | 5 error 9 | 6 error System Linux 3.10.18 10 | 7 error command "/usr/local/bin/node" "/usr/local/bin/npm" "test" 11 | 8 error cwd /home/ian/githubfollowers/test/fixtures 12 | 9 error node -v v0.10.35 13 | 10 error npm -v 1.4.28 14 | 11 error path /home/ian/githubfollowers/test/fixtures/package.json 15 | 12 error code ENOENT 16 | 13 error errno 34 17 | 14 verbose exit [ 34, true ] 18 | -------------------------------------------------------------------------------- /test/user.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var P = require('bluebird'); 3 | var db = require('../lib/db'); 4 | var expect = require('chai').expect; 5 | var user = require('../lib/user'); 6 | var fixtures = require('./fixtures'); 7 | var sinon = require('sinon'); 8 | 9 | describe('isFollowing', function() { 10 | var joe = fixtures.newJoe(); 11 | joe.github.user.getFollowUser = function(any, cb) { 12 | if (any.user === 'test') cb(null, true); 13 | cb('error', false); 14 | }; 15 | 16 | it('should return true if following', function(done) { 17 | joe.isFollowing('test').then(function(res) { 18 | console.log(res); 19 | expect(res).to.be.true; 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should return false if not following', function(done) { 25 | joe.isFollowing('notfollowing').then(function(res) { 26 | expect(res).to.be.false; 27 | done(); 28 | }); 29 | }); 30 | 31 | }); 32 | 33 | describe('follow', function() { 34 | var joe = fixtures.newJoe(); 35 | joe.github.user.followUser = function(obj, cb) { 36 | if (obj.user === 'success') return cb(null, 'followed'); 37 | if (obj.user === 'fail_req') return cb('fail', null); 38 | cb(null, null); // fail res 39 | }; 40 | var users = db.get('users'); 41 | P.promisifyAll(users); 42 | 43 | it('should update the database if follow user succeeds', function(done) { 44 | joe.follow('success').then(function(res) { 45 | return users.insertAsync({ 46 | name: 'success' 47 | }); 48 | }).then(function() { 49 | return users.findOne({ 50 | name: 'success' 51 | }, function(err, doc) { 52 | expect(doc.name).to.equal('success'); 53 | }); 54 | }).then(function() { 55 | return users.remove({ 56 | name: 'success' 57 | }); 58 | }).then(function() { 59 | done() 60 | }); 61 | }); 62 | 63 | it('should not update if follow user request fails', function(done) { 64 | joe.follow('fail_req').catch(function(err) { 65 | users.findOne({ 66 | name: 'fail_req' 67 | }, function(err, doc) { 68 | expect(doc).to.be.null; 69 | done(); 70 | }); 71 | }); 72 | }); 73 | 74 | it('should not update if follow user fails', function(done) { 75 | joe.follow('fail').then(function(res) { 76 | users.findOne({ 77 | name: 'fail' 78 | }, function(err, doc) { 79 | expect(doc).to.be.null; 80 | done(); 81 | }); 82 | }); 83 | }); 84 | 85 | }); 86 | 87 | describe('unfollow', function() { 88 | var joe = fixtures.newJoe(); 89 | joe.isFollowing = function() { 90 | return new P(function(resolve) { 91 | resolve(true); 92 | }); 93 | }; 94 | joe.github.user.unFollowUser = function(obj, cb) { 95 | if (obj.user === 'success') return cb(null, 'unfollowed'); 96 | if (obj.user === 'fail_req') return cb('fail', null); 97 | cb(null, null); // fail res 98 | }; 99 | var users = db.get('users'); 100 | P.promisifyAll(users); 101 | 102 | it('should update the database if unfollow user succeeds', function(done) { 103 | joe.unfollow('success').then(function(res) { 104 | return users.insertAsync({ 105 | name: 'success' 106 | }); 107 | }).then(function() { 108 | return users.findOne({ 109 | name: 'success' 110 | }, function(err, doc) { 111 | expect(doc.name).to.equal('success'); 112 | }); 113 | }).then(function() { 114 | return users.remove({ 115 | name: 'success' 116 | }); 117 | }).then(function() { 118 | done(); 119 | }); 120 | }); 121 | 122 | it('should not update if unfollow user request fails', function(done) { 123 | joe.unfollow('fail_req').catch(function(res) { 124 | users.findOne({ 125 | name: 'fail_req' 126 | }, function(err, doc) { 127 | expect(doc).to.be.null; 128 | done(); 129 | }); 130 | }); 131 | }); 132 | 133 | it('should not update if unfollow user fails', function(done) { 134 | joe.unfollow('fail').then(function(res) { 135 | users.findOne({ 136 | name: 'fail' 137 | }, function(err, doc) { 138 | expect(doc).to.be.null; 139 | done(); 140 | }); 141 | }); 142 | }); 143 | 144 | }); 145 | 146 | describe('star', function() { 147 | 148 | it('should run star on the repo', function(done) { 149 | var joe = fixtures.newJoe(); 150 | joe.github.repos.star = function(obj, cb) { 151 | expect(obj.user).to.equal('user'); 152 | expect(obj.repo).to.equal('repo'); 153 | cb(); 154 | }; 155 | joe.star('user', 'repo').then(done, done); 156 | }); 157 | 158 | }); 159 | 160 | describe('fromModel', function() { 161 | it('should set correct params', function() { 162 | 163 | var model = { 164 | login: 'aubhaze', 165 | token: 'fake', 166 | avatar_url: 'asdf' 167 | }; 168 | var res = user.fromModel(model); 169 | 170 | expect(res.login).to.equal(model.login); 171 | expect(res.model).to.eql(model); 172 | 173 | }); 174 | }); 175 | 176 | describe('validate', function() { 177 | 178 | it('should return false for an invalid oauth token', function(done) { 179 | var aub = user.fromModel({ 180 | login: 'aubhaze', 181 | token: 'fake', 182 | model: {} 183 | }); 184 | aub.validate().then(function(res) { 185 | expect(res).to.be.false; 186 | done(); 187 | }, done); 188 | }); 189 | 190 | }); 191 | --------------------------------------------------------------------------------