How to Tweet from NodeJS
Published 2013-7-16Watch 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
- Go to https://dev.twitter.com/apps/
- Create an app with the domain http://local.example.com:4000
- From Settings add Read, Write and Access direct messages
- From Settings add Allow this application to be used to Sign in with Twitter
- 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
Did I make your day?
Buy me a coffee
(you can learn about the bigger picture I'm working towards on my patreon page )