Skip to content

mustardamus/follow-ng

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

79 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Follow-ng

Please note that this application is only for educational purposes! Twitter's Rules does not allow such an app to be used:

It is not allowed to ... repeatedly follow and unfollow people, whether to build followers or to garner more attention for your profile;

Well, that is exactly what this app is for. I coded it as an exercise for myself and to test out my Yeoman Grail Generator. I can tell you right away that your app will be banned after a certain amount of time. So don't even bother setting it up - but learn from it.

Resources

Workers

Workers are continues and work separate from the server. They are living in ./workers and have a general loader in ./workers/index.coffee.

Workers are called with the config, initialized models, helpers, the account to process, the log function and the initialized Twit module as arguments. They have to export a class that is called by the loader in an interval.

Define them like so (./workers/search.coffee):

module.exports = class SearchWorker
  constructor: (@config, @models, @helpers, @account, @log, @twit) ->

And set the interval in ./server/config.coffee like so:

workers:
  intervals: # in minutes
    search: 30

Then you would call it like this:

coffee workers --worker=search

Search Worker

This worker searches for every Term associated with a Account and puts the found Friend in the database, if it does not yet exist. If it doesn't exist, it also fetches the user information from Twitter.

Update Worker

This worker get all Followers and Friends of a Account. It then splits the ID's into Followback (Account follows Friend, Friend follows Account), Followers and Friends. Then it checks if the Friend is already in the Database, if not it fetches the information from Twitter and stores it in the database.

Backfollow Worker

This worker finds all friends that are following the account but the account does not follow them yet. It follows them and updates the field on the model. Easy peasy.

Unfollow Worker

This worker finds all friends that are followed but didn't follow back. It checks the date when the following happened, and if the time between now and the following is greater than the one specified in the settings, and the friend is unfollowed and the unfollowed field on the model is set to true.

Follow Worker

This worker will find every potential friend added by the Search Worker and randomly follow people. The followed field, as well as the time when the follow has happened, is set.

Reset Worker

This worker resets the hits for following und unfollowing once a day

Application Stack

This environment is intended to be used in a modular way. Everything is a Component and should work and be testable independently.

Generated with the Grail Generator for Yeoman.

Tasking

Task management is done with Gulp, so your project is easily extendible. This stack already uses some Gulp Plugins:

Scripting

Codes are written mainly in CoffeeScript and bundled together with Browserify. Out of the box it comes with some transforms for Browserify:

Styling

Styles are written mainly in Stylus.

Watching

Watching for file changes is crucial if you pre-compile the Scripts and Styles. For the Script re-bundle Watchify is in charge.

Since the Styles are not in the JS bundle, Chokidar is used to watch and rebuild them. gulp.watch isn't used here, since it does not pick up newly created files.

For the rest of the files (HTML, Images) it uses gulp.watch.

Serving

The generated and bundled files will be served with BrowserSync. That means the Application is automatically reloaded if Scripts or Styles are changing.

Testing

Component tests written in CoffeeScript, bundled by Browserify and run by Mocha.

Gulp Tasks

Source of all tasks is ./client and destination is ./public.

gulp html

Copies the HTML Entry Point.

gulp fonts

Copies fonts. This tasks copies the fonts of Font Awesome included by Semantic-UI by default, but note that you need to run yo grail:extend, so the files are available.

gulp stylus

Bundles the Style Entry Point with Stylus. Also prefix CSS3 properties with Autoprefixer.

gulp images

Copies all images.

gulp browserify

Bundles the Script Entry Point with Browserify. It executes several transforms:

  • coffeeify - transforms the CoffeeScript to JavaScript
  • html2js - transforms the HTML templates to JavaScript strings
  • debowerify - require() modules installed by Bower
  • deamdify - require() modules that are wrapped by AMD

gulp browserify-watch

Watches files in the source directory and re-bundles the Script Entry Point.

gulp watch

  • Run browserify-watch
  • Run stylus whenever a ./client/**/*.styl changes
  • Run images whenever a file in ./client/images/* changes
  • Run html whenever a ./client/*.html changes

gulp server

Starts a BrowserSync web server. Also starts the watch task. Whenever a file in the source directory changes, the Script or Style Entry Point is re-bundled and the browser reloaded. Styles are directly injected in the page without a reload.

gulp test

Bundles the Application Components and Tests together and run them in Mocha.

gulp test-watch

Whenever a Application or Test file changes re-run the test Task.

gulp build

Run browserify, stylus, images and html. gulp without a task name will also run this task.

gulp production

Run this task if you want to release the Application for the audience. Note that you need to run gulp build before!

  • Run browserify and minify JS with Uglify
  • Run stylus and minify CSS with CSSO
  • Run images and minify them with Imagemin

gulp bump

Bumps up the version number in package.json.

Application Structure

This is a one page application. It has three different entry points.

HTML entry point - ./client/index.html

This is your typical index.html page which is loaded first. It has references to the Script and Stylesheet entry points.

Markup that is initially loaded (the Layout), is stored here.

Script entry point - ./client/index.coffee

Here you initialize components. Since the Script Bundle is packed with Browserify you can simply require() the components you want to use:

Post = require('./components/post')
post = new Post

It is recommended to require() third party dependencies in the components rather than having globals. But if you install the libraries with Bower, the respective global will be defined without declaring it:

require('jQuery') # $ is globally defined

However, if you declare it this way your tests may brake because they are run in Node.js by default (todo: run them in PhantomJS).

Stylesheet entry point - ./client/index.styl

Here you initialize stylesheets, third party stylesheets and variables. Stylus will take care of the bundling, so all you need is @import.

For third party styles, include a .css file in a relative path:

@import '../bower_components/normalize-css/normalize.css'

To import a module style:

@import './components/post/style'

Note that you don't need to include every single Component style since by default every style.styl from the ./client/components is imported.

If you define variables, you can use them in your imported components:

backgroundColor = #eee

And in ./client/components/post/style.styl:

@import './colors'
body
  background: backgroundColor

Component Structure

A component can have three different things: A Script, a Template, and a Style. All three are optional, since it's in your hands how you initialize each part of a component.

Naming Convention

Again, you can name them like you want, really. I recommend this structure:

./client/components/[component-name]
  /index.coffee
  /template.html
  /style.styl

Why this naming? You can leave out the file extension when require()ing them (which would not possible if every component-part has the same name):

post     = require('../post')    # respectively index.coffee
template = require('./template') # respectively template.html

With the component-name as identifier, and index.coffee as Script entry point, you describe with the filename which part of the component you want to require(): post (respectively index.coffee), post/template and post/style.

A disadvantage of this method is that it might not be clear if you are editing multiple components in your editor. But you should work on one component at a time anyway.

Testing a Component

You can and should test your Components. Just like the Application Script Entry Point, the Tests have a Entry Point too in ./test/client/index.coffee. There you require() the components and tests:

postTest = require('./components/post')

Then just create the component test a file named like the component: ./test/client/components/post.coffee:

should = require('should')
Post   = require('../../../client/components/post')
post   = new Post

describe 'Post Component', ->
  it 'should have the correct template', ->
    post.template.should.equal 'testing'

To bundle the Application and Tests, and run it in Mocha:

gulp test

To automatically re-run the test task whenever Application or Test files change, run:

gulp test-watch

Workflow

Using third party Scripts

Third party libraries can be either installed with NPM or Bower. In the end, you just need to require() the library.

NPM

npm install moment --save

Bower

bower install zepto --save

Then you can require the libraries wherever you want (usually in the Component):

$      = require('zepto')
moment = require('moment')

Manually

In the rare case you can't install a library from NPM or Bower, you also can require() the .js from everywhere:

lib = require('../path/to/lib.js')

Using third party Styles

Install it via Bower:

bower install foundation --save

And @import it in ./client/index.styl:

@import '../bower_components/foundation/css/foundation.css'

Client Extensions

$root Component

The entry point for the Vue.js application is ./client/components/$root/index.coffee. This component is initialized in a <div id="app"/> container on the <body> of the ./client/index.html page, by the Script entry point ./client/index.coffee.

Normally you should only touch ./client/index.coffee to add new bootstrapping functionality or glogbally defined libraries.

Use ./client/components/$root/data.coffee to define data that are accessible by every component via @$root.$data.

If you need to trigger events in components that are not related you should use @$root.$dispatch() and @$root.$on().

$layout Component

Components should be defined and initialized in ./client/components/$layout/index.coffee and ./client/components/$layout/template.html. Global styles should go in ./client/components/$layout/style.styl.

$router Component

The $router Component uses Director and sets @$root.$data.currentPage whenever the page changes. The /#/ route will become home. /#/about, for example, will become about.

Define your custom routes in ./client/components/$router/index.coffee.

page-home Component

This acts as a example component. It is defined and initialized in the $layout component, and is loaded whenever @$root.$data.currentPage === 'home'.

It's recommended to keep the page-* convention when you create other top-level pages.

Ideally, you would not need jQuery in combination with Vue.js. But face it, you learned it for years and it is the fastest way to interact with the DOM. You can use thousands of jQuery Plugins out of the box. Also does the Semantic-UI Plugins rely on jQuery.

Get your app running as fast as possible, adjust, refactor and speed up later.

Semantic-UI stylesheets are included in ./client/index.styl separately. Turn them on and off by commenting them in or out.

Semantic-UI also offers a bunch of jQuery Plugins. They are included separately in ./client/index.coffee. Just like the stylesheets, turn them on and off as needed.

Note that Semantic-UI includes Font Awesome icons.

If you are running a Socket.io Server (yo grail:server, for starters) you can use Socket.io. If the web application is serving from gulp server (port 7891), the global window.io object is stubbed and don't do anything other than printing a warning to console.log.

FastClick library for eliminating the 300ms delay between a physical tap and the firing of a click event on mobile browsers.

Cheerio is a jQuery like helper for Node.js. This makes running tests on HTML code pretty easy.

Should.js is a assertion library that reads better than the assert functions that come with Node.js.

Create a Vue.js Component

Use the command yo grail:create to create a component in ./client/components. You can choose which parts you want to create: Script, Template, Style and/or Test.

Then you just require() the Script or Template, @import() the Style wherever it is needed. Component names should be all lowercase and divided by a dash - if multiple words.

Server Extensions

Entry Point

Bootstrapping is happening in ./server/index.coffee. You usually don't need to touch this file. If you want to extend the functionality of the Server you should use the ./server/initialize directory (see below).

Configuration

All configuration goes in ./server/config.coffee and is passed along to initilization and routes (see examples below).

The default port of the Server is 7799. The default static directory is ./public - generated by gulp build (you want to run gulp watch to pick up file changes while developing).

Initilization

The Entry Point will load and initialize all files that are in ./server/initialize. For example ./server/initialize/app.coffee will initialize the JSON Body Parser and the static directory:

module.exports = (config, helpers, io, models) ->
  @use express.static(config.server.publicDir)
  @use bodyParser.json()

The context (@/this) is the express() app that is initialized in the Entry Point.

Routes

Pretty much the same as the Initilization files, the Entry Point will load all files in ./server/routes. Split the files depending on your resources and define all related routes in them. For example ./server/routes/app.coffee:

module.exports = (config, helpers, io, models) ->
  @get '/hello/:name', (req, res) ->
    res.json { str: helpers.app.sayHello(req.params.name) }

  @post '/whatever, (req, res) ->

Helpers

Helpers are commonly used functions that can be shared between initilization, routes and models. They are passed to the exported function as seen in the examples above.

All helpers will be loaded from the directory ./server/helpers.

The name of the file is important, as it's used to populate the helpers object. For example ./server/helpers/app.coffee:

module.exports =
  sayHello: (toName) ->
    "Hello #{toName}!"

Will be available as helpers.app.sayHello(). A file ./server/helpers/string.coffee would be available as helpers.string.*.

Helpers should only work with raw data and should not interact with the Express app or models in any way.

Start Server for Development

When in development environment, use npm start. This will start a Forever process that still logs to STDOUT. It will watch the ./server directory for any file changes and restarts the server.

You'd still need to reload the browser manually. Unfortunately I haven't found a reliable solution for this yet.

Lodash is already installed for convinience. Just do _ = require('lodash') anywhere and hack away.

A Socket.io Server is automatically initialized on the Entry Point and is served at the same port as the HTTP Server. The io object is passed to the initilization and the routes, see the examples above.

MongoDB with Mongoose (Optional)

You can define Mongoose models in ./server/models. For example ./server/models/count.coffee:

module.exports = (helpers) ->
  @model 'Count',
    visits: Number

The context (@/this) is the mongoose object defined in the Entry Point. Just make sure that the mongoose.model definition is returned.

Models are initialized in the models object and passed to initilization as well as routes. Just as the helpers it is important how you name the files, as it is the key the model is initialized with. The example above would be located at models.count. A usage example would be:

module.exports = (config, helpers, models) ->
  @get '/count', (req, res) ->
    models.count.findOne {}, (err, count) ->
      unless count
        count = new models.count({ visits: 0 })

      count.visits += 1
      count.save()

      res.json count.toJSON()

The Server Entry Point only tries to connect to the database defined in the config file if there are any model files. So you might use the Server without any database connection by leaving the ./server/models directory empty or remove it altogether.

Registration and Authentication

A boilerplate for basic user registration and authentication. This is based on yo grail:extend and yo grail:server.

Please note that this is a boilerplate to get started. In the future you likely want to implement more security measures like brute-force protection, password-strength and e-mail confirmation.

Backend

This is a custom implementation with jsonwebtoken and bcrypt.

POST /register

On this route a user can register with a username and a password. It is checked if the user already exist.

POST /login

On this route a user can login and generate a token that is delivered to the frontend and acts as a session. Logging out just happens on the frontend by destroying the token.

GET /users/me

This route will return the information of the current logged in user.

POST /users/me

On this route a user can update her information: username, e-mail and password. It is checked if the username and e-mail already exists. To change the password the user must provide the current password.

auth Middleware

You can protect routes by the authentication middleware like this:

module.exports = (config, helpers, io, models) ->
  auth = require('../middleware/auth')(config, helpers, models)

  @get '/inside', auth, (req, res) ->
    res.json req.user

If the authentication fails, a 403 code is delivered to the client and the defined callback is never happening. If the authentication succeeds, req.user is populated with the Mongoose User Model.

Frontend

Requests are happening exclusively via AJAX.

/#/register

On this page is the register form. Username, password and password check. With form validation on the client-side. Once a user is registered he is already logged in because the server returns the token.

/#/login

On this page a user can login with username and password. If successfully logged in, the server will return the token.

/#/user

On this page a user can see and edit her information. If the e-mail is missing, a extra message is displayed saying to provide a e-mail. Form validation is happening on the client-side. To change the password the current password must be provided.

Determine if a user is logged in

Once a user is successfully logged in (either by registering, logging in or already set token), there are multiple data available on the @$root.$data object:

loggedIn    : true/false   # a simple boolean stating if the user is logged in
currentUser : null/object  # if logged in, the user information (except password) is stored here
currentToken: null/string  # the token of the current logged in user

About

A app for automatic following on Twitter which you are not allowed to use by their TOS

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published