Watch on YouTube: youtu.be/BmPsnZbmj0M

Disclaimer: this video is raw, unrehersed, and long.

There are three things that we'll do here:

  • Authorize our App for use with Twitter
  • Authenticate our App using Twitter
  • Send tweets and direct messages

TL;DR

For those that don't care to beat around the bush and would rather get straigt to the code, here ye be: https://github.com/coolaj86/node-twitter-demo

Edit your hosts file

Get into /etc/hosts and add local.example.com 127.0.0.1.

If you have a real domain to experiment on, use that instead, but you need a non-localhost domain for twitter apps to work at all.

For example:

sudo vim /etc/hosts

Create a Skeleton ExpressJS App

First you must have express installed (or connect, which is what I generally prefer).

npm install -g express

Then run the express command to generate the barebones app template.

# Generate
express twitter-demo

# Get into that directory
pushd twitter-demo/

# Finish the setup by installing dependencie
# (listed in package.json)
npm install

# Set the PORT environment variable
# (or accept the default of 3000)
export PORT=4040

# Run the app to test and see that it works
node ./app.js

You can connect to http://local.example.com:4040 and see that your app runs.

Create an App on Twitter

You'll need to

  1. Go to https://dev.twitter.com/apps/
  2. Create an app with the domain http://local.example.com:4000
  3. From Settings add Read, Write and Access direct messages
  4. From Settings add Allow this application to be used to Sign in with Twitter
  5. From the Details page copy the key and secret into a new file, config.json

config.json: (goes in the same directory as app.js)

{ "consumerKey": "..."
, "consumerSecret": "..."
}

Add Passport for Twitter Authorization

First we just need to add passport, which is nothing crazy

First the usual installation

npm install --save passport passport-twitter

And for brevity's sake I've used ... to denote a block of code that isn't copied into this example.

You can see the full code in the link provided.

views/index.jade: https://github.com/coolaj86/node-twitter-demo/blob/master/views/index.jade

extends layout

block content
  h1= title
  p Welcome to #{title}

  //- Here's the link we add
  a(href="/twitter/authz") Authorize this Twitter App
  br

All we're doing there is adding a link to direct the user to our passport middleware which will redirect them to twitter and allow them to authorize this app to use their account.

app.js: https://github.com/coolaj86/node-twitter-demo/blob/master/app.js

"use strict";

var express = require('express')
  , routes = require('./routes')
...
  // server info
  , domain = "local.example.com"
  , port = process.env.PORT || 3000
  // passport / twitter / oauth stuff
  , config = require('./config')
  , passport = require('passport')
  , TwitterStrategy = require('passport-twitter').Strategy
  , twitterAuthn
  , twitterAuthz
  // poor man's database stub
  , user = { id: "foo" }
...

// Because we're using Direct Messaging we'll need 2 twitter strategies
//  * Twitter Authorization
//  * Twitter Authentication

// We'll just worry about *authorization* right now
// Note the devations from the example on the passport website
twitterAuthz = new TwitterStrategy({
    consumerKey: config.consumerKey
  , consumerSecret: config.consumerSecret
  // Note `authz`, not `auth`
  , callbackURL: "http://" + domain + ":" + port + "/authz/twitter/callback"
  // We override the default authentication url with the authorize url
  , userAuthorizationURL: 'https://api.twitter.com/oauth/authorize'
  },
  function(token, tokenSecret, profile, done) {
    // Database logic should go here, but we'll just stub it for now
    user.token = token;
    user.tokenSecret = tokenSecret;
    user.profile = profile;
    console.log('Houston, we have a login');
    console.log(profile);
    done(null, user);
  }
);
// The default name is 'twitter', but we need two.
twitterAuthn.name = 'twitterAuthn';

passport.use(twitterAuthn);

// just some stubs for saving and retrieving users
// these need to exist for passport.session() to work
passport.serializeUser(function(_user, done) {
  done(null, user.id);
});
passport.deserializeUser(function(id, done) {
  done(null, user);
});

// Moved the port logic up top where it belongs
app.set('port', port);
...
app.use(express.methodOverride());
// Passport needs express/connect's cookieParser and session
app.use(express.cookieParser());
app.use(express.session({ secret: "blahhnsnhoaeunshtoe" }));
app.use(passport.initialize());
app.use(passport.session());
// Passport MUST be initialize()d and session()d before the router
app.use(app.router);

...

// We can add these routes after the
app.get('/twitter/authz', passport.authenticate('twitterAuthn'));
app.get(
  '/twitter/authz/callback'
, passport.authenticate(
    'twitterAuthz'
    // this is just a visual cue for our testing purposes
    // you'd want to change this to some useful page
  , { successRedirect: '/z-success'
    , failureRedirect: '/z-failure'
    }
  )
);

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

At this point if all goes well you can quit your app and run it again PORT=4040 node app, go to http://local.example.com:4040, and see your beautiful Authorize Link.

If you Authorize, you'll then be able to do other stuff - like tweet and whatnot.

The problem with authorization and twitter is that twitter will always ask the user to authorize, even if they have already authorized previously... more on that later.

Creating Status Updates (tweets) and Direct Messages

All we have to do with our view is add another link. (it should actually be a form, but for this demo we're doing things The Wrong Way™)

views/index.jade: https://github.com/coolaj86/node-twitter-demo/blob/master/views/index.jade

...

  a(href="/twitter/authz") Authorize this Twitter App
  br
  //- note that the user must first authorize before this will work
  a(href="/twitter/tweet") Tweet to @coolaj86
  br

In the app there are also relatively few changes

app.js: https://github.com/coolaj86/node-twitter-demo/blob/master/app.js

"use strict";

var express = require('express')
  , routes = require('./routes')
...
  // poor man's database stub
  , user = { id: "foo" }
  , OAuth= require('oauth').OAuth
  , oa
  ;

// We init OAuth with our consumer key & secret just like with passport
function initTwitterOauth() {
  oa = new OAuth(
    "https://twitter.com/oauth/request_token"
  , "https://twitter.com/oauth/access_token"
  , config.consumerKey
  , config.consumerSecret
  , "1.0A"
  , "http://" + domain + ":" + port + "/authn/twitter/callback"
  , "HMAC-SHA1"
  );
}

// In order to tweet we must have the user's token and secret
// (which we've stored in our poor man's db
// Notice how easy OAuth is, we don't even need a library
// https://dev.twitter.com/docs/api/1/post/statuses/update
function makeTweet(cb) {
  if (!user.token) {
    console.error("You didn't have the user log in first");
  }
  oa.post(
    "https://api.twitter.com/1.1/statuses/update.json"
  , user.token
  , user.tokenSecret
  // We just have a hard-coded tweet for now
  , { "status": "How to Tweet & Direct Message using NodeJS http://blog.coolaj86.com/articles/how-to-tweet-from-nodejs.html via @coolaj86" }
  , cb
  );
}

...

// This is where we handle the tweet link
// (which should have been a form with user input)
app.get('/twitter/tweet', function (req, res) {
  makeTweet(function (error, data) {
    if(error) {
      console.log(require('sys').inspect(error));
      res.end('bad stuff happened, none tweetage');
    } else {
      console.log(data);
      res.end('go check your tweets!');
    }
  });
});

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

Adding direct messaging (or whatever else)

If you're going to be doing a lot of twitter stuff, it's probably a good idea to look for a library to fork and add any methods that you need that it's missing (and create a Pull Request for them to the maintainer).

If you're just doing onesy/twosy stuff you might continue in the fasihon shown above and as seen here: https://github.com/coolaj86/node-twitter-demo/blob/master/app.js#L49

app.js: https://github.com/coolaj86/node-twitter-demo/blob/master/app.js

...

// We just change the url and parameters to create a DM function
function makeDm(sn, cb) {
  oa.post(
    "https://api.twitter.com/1.1/direct_messages/new.json"
  , user.token
  , user.tokenSecret
  , {"screen_name": sn, text: "test message via nodejs twitter api. pulled your sn at random, sorry."}
  , cb
  );
}

...

// The route for the link that will send a DM
app.get('/twitter/direct/:sn', function (req, res) {
  // Note that this should also be a form, but doing it
  // The Wrong Way (TM), I've decided to use a parameter
  makeDm(req.params.sn, function (error, data) {
    if(error) {
      console.log(require('sys').inspect(error));
      res.end('bad stuff happened (dm)');
    } else {
      console.log(data);
      res.end("the message sent (but you can't see it!");
    }
  });
});

...

Caveat of Authn vs Authz

First off, if you haven't followed the tutorial or gotten the demo running, this may not yet make sense to you - fair warning.

For the authn code, just take a look at the github repo and the twitterAuthn strategy: https://github.com/coolaj86/node-twitter-demo/blob/master/app.js#L65

Here's the problem:

If you need to use Read, Write and Access direct messages, you MUST use twitter's /oauth/authorize in order to get direct message privileges. Otherwise you will not have them and /oauth/athenticate will force Sign In every time a user uses your app (rather than just the first time to authorize it).

The solution isn't as elegant as we'd like:

Have the user authenticate first and look the user up in your database. You should create some sort of boolean in the db, such as user_has_authorized and if it isn't true, redirect the user to authorize before you allow them to attempt to send a direct message.

It would be good to do this the very first time they log in, but then you'll have them prompted to Sign In and then to Authorize App immediately afterwards, which looks not-smooth.

Lastly, if you aren't using Direct Messages, you can get away with just authenticate and it will only explicitly ask permission the first time and auto-redirect to success every subsequent time.

P.S. The clever iFrame trick that would have worked in days of yore won't work with the new HTTP headers for enhanced browser security. (I tried this in the video)


By AJ ONeal

If you loved this and want more like it, sign up!


Did I make your day?
Buy me a coffeeBuy me a coffee  

(you can learn about the bigger picture I'm working towards on my patreon page )