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.
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
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.
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.
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.
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.
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.
This worker resets the hits for following und unfollowing once a day
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.
Task management is done with Gulp, so your project is easily extendible. This stack already uses some Gulp Plugins:
- gulp-util -
noop()
and logging - gulp-concat - To concat files together
- gulp-imagemin - Wrapper for imagemin, to optimize images
- gulp-stylus - Wrapper for Stylus, to compile to CSS
- gulp-autoprefixer - Wrapper for Autoprefixer, to vendor prefix CSS3
- gulp-csso - Wrapper for CSSO, to minify CSS
- gulp-uglify - Wrapper for Uglify, to minify JavaScript
- gulp-spawn-mocha - To run tests with Mocha
- gulp-bump - To bump up the version
number in
package.json
Codes are written mainly in CoffeeScript and bundled together with Browserify. Out of the box it comes with some transforms for Browserify:
- coffeeify - To compile CoffeeScript to JavaScript
- html2js-browserify - To compile HTML templates to JavaScript strings
- debowerify - To use Bower components in Browserify
- deamdify - To use AMD modules in Browserify
Styles are written mainly in Stylus.
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.
The generated and bundled files will be served with BrowserSync. That means the Application is automatically reloaded if Scripts or Styles are changing.
Component tests written in CoffeeScript, bundled by Browserify and run by Mocha.
Source of all tasks is ./client
and destination is ./public
.
Copies the HTML Entry Point.
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.
Bundles the Style Entry Point with Stylus. Also prefix CSS3 properties with Autoprefixer.
Copies all images.
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
Watches files in the source directory and re-bundles the Script Entry Point.
- 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
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.
Bundles the Application Components and Tests together and run them in Mocha.
Whenever a Application or Test file changes re-run the test
Task.
Run browserify
, stylus
, images
and html
. gulp
without a task name will
also run this task.
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
Bumps up the version number in package.json
.
This is a one page application. It has three different entry points.
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.
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).
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
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.
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.
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
Third party libraries can be either installed with NPM or Bower. In the end, you
just need to require()
the library.
npm install moment --save
bower install zepto --save
Then you can require the libraries wherever you want (usually in the Component):
$ = require('zepto')
moment = require('moment')
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')
Install it via Bower:
bower install foundation --save
And @import
it in ./client/index.styl
:
@import '../bower_components/foundation/css/foundation.css'
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()
.
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
.
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
.
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.
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.
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).
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).
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.
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 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.
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.
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.
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.
This is a custom implementation with jsonwebtoken and bcrypt.
On this route a user can register with a username and a password. It is checked if the user already exist.
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.
This route will return the information of the current logged in user.
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.
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.
Requests are happening exclusively via AJAX.
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.
On this page a user can login with username and password. If successfully logged in, the server will return the token.
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.
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