If you have created a site or web app with Node and Express, you may want to share information about new content or other notifications with site users and subscribers across multiple channels. One of the more common channels used for this kind of activity is Twitter. Setting up a node app to tweet is relatively straight forward and once you’ve gone through the authentication process once most other actions become fairly easy to figure out. That said, most documentation that I’ve found doesn’t do a great job at walking an individual through that first setup and authentication process. Follow along and we’ll get a basic setup done with explanation of what’s happening.
Setup Prerequisites
First and before anything else, if you don’t already have a twitter developer account, you’ll need to sign up for and create one.
After creating a developer portal account, you’ll need to create a project/app and generate your API keys. Likewise, you’ll want to configure your “User authentication settings” and enable the correct version of OAuth for your purposes. Since sending a tweet is still on the 1.1 API, we need to enable OAuth 1.0a for our purposes.
You’ll want to fill in the Callback URI section as well with the url you want to use at your site for callback functions when authenticating. It’s important to fill this is, as only urls that you have configured as allowed in the developer portal will be accepted as valid callbacks by the API.
We’re going to use a module called twitter-api-v2 to simplify things, so change to your project directory and run ‘npm install twitter-api-v2’
Cool. Now that the prerequisite steps are handled, we can get into actually doing something.
Setup your nodejs module
For ease of use, we’re going to put all twitter api related functions in a separate module so that we can call them from various routes as needed. We’re going to setup our module to keep all twitter account information attached to user accounts so that our app can tweet to different twitter accounts. Here’s the module file we put together:
Module file - twitterapi.js
require('dotenv').config(); const pgdb = require('./postgres'); const {TwitterApi} = require('twitter-api-v2'); const twitterappkey = process.env.twitterappkey const twitterappsecret = process.env.twitterappsecret const sitetld = process.env.sitetld
const client = new TwitterApi({ appKey: twitterappkey, appSecret: twitterappsecret });
async function setuserauth(currentuser, authLink){ usersql = "UPDATE users SET toauth_token = $1, toauth_secret = $2 WHERE id = $3"; uservals = [authLink.oauth_token, authLink.oauth_token_secret, currentuser.userid]; await pgdb.writequery(usersql, uservals); }
async function setuseraccess(useraccount, accessToken, accessSecret){ var tsettings = await gettwittersettings(useraccount); if (tsettings){ var twittername = "@" + tsettings.screen_name }else{ var twittername = "" } usersql = "UPDATE users SET taccess_token = $1, taccess_secret = $2, twittername = $3 WHERE id = $4"; uservals = [accessToken, accessSecret, twittername, useraccount.id]; await pgdb.writequery(usersql, uservals) }
async function genlinkv1(currentuser){ try{ var authurl = sitetld + "/user/twitauthcallback" const authLink = await client.generateAuthLink(authurl); // Redirect your client to authLink.url if (authLink){ console.log('Please go to', authLink.url + "\n"); console.log('Token: ' + authLink.oauth_token + "\n"); console.log('secret: ' + authLink.oauth_token_secret + "\n"); await setuserauth(currentuser, authLink); return authLink; }else{ return null; } }catch{ console.log("Error encountered") return null; } }
// ... user redirected to https://your-website.com?oauth_token=XXX&oauth_verifier=XXX after user app validation
async function gentokenv1(useraccount, twitterpin){ console.log("Trying with verifier " + twitterpin) var errormessages = [] if (!useraccount.toauth_token){ errormessages.push("Could not retrieve oauth_token") } if (!useraccount.toauth_secret){ errormessages.push("Could not retrieve oauth_token_secret") } if (errormessages.length < 1){ const connectorClient = new TwitterApi({ appKey: twitterappkey, appSecret: twitterappsecret, accessToken: useraccount.toauth_token, accessSecret: useraccount.toauth_secret, }); // Validate verifier to get definitive access token & secret try{ const { accessToken, accessSecret, client: loggedClient } = await connectorClient.login(twitterpin);
console.log('Access token and secret for logged client:', accessToken, accessSecret, client); await setuseraccess(useraccount, accessToken, accessSecret) }catch(error){ console.log(error); //errormessages.push(error) errormessages.push("Could not complete authorization with that PIN. Plase start over.") } } return errormessages }
async function gettwittersettings(useraccount) { const sclient = new TwitterApi({ appKey: twitterappkey, appSecret: twitterappsecret, accessToken: useraccount.taccess_token, accessSecret: useraccount.taccess_secret, }); const settings = await sclient.v1.accountSettings(); console.log(settings) return settings; }
async function sendusertweet(useraccount, tweetstring){ const client = new TwitterApi({ appKey: twitterappkey, appSecret: twitterappsecret, accessToken: useraccount.taccess_token, accessSecret: useraccount.taccess_secret, }); client.v1.tweet(tweetstring); }
module.exports = { genlinkv1: (currentuser) => genlinkv1(currentuser), gentokenv1: (useraccount, twitterpin) => gentokenv1(useraccount, twitterpin), gettwittersettings: (useraccount) => gettwittersettings(useraccount),
sendusertweet: (useraccount, tweet) => sendusertweet(useraccount, tweet) }
|
Note that you’ll need to setup some method of storing your twitter key and secret for your app. In the example above we are storing both in an .env fie and using dotenv to load the variables. For safety and security, do not keep those variables in a file that gets pushed to your repository. Using true environment variables is typically best practice, but for development and testing a .env file should be safe enough, provided it is excluded in your .gitignore
Setup Routes and Views
Now that we have a module file, we can reference those functions from our routing file. Since we’re storing the twitter account info attached to user accounts, we’ll set up our routes and views as part of our user model for simplicity. Here’s our user route, we’ll walk through more of what is happening within it and how you might want yours to differ in a minute:
Route - user.js
const express = require('express'); const pgdb = require('../../db/postgres'); const router = express.Router(); var bodyParser = require('body-parser') var urlencodedParser = bodyParser.urlencoded({ extended: false }) const cookieparser = require("cookie-parser"); const sessionstore = require('../../db/sessionstore'); const sitename = process.env.sitename; const useraccounts = require('../../db/useraccounts'); const twitterapi = require('../../db/twitterapi');
router.get('/settings', async (req, res) => { var sessionid = req.cookies.sessionid var currentuser = await sessionstore.getsession(sessionid); if (currentuser){ if (currentuser.securitylevel >= 50){ var useraccount = await useraccounts.getbyusername(currentuser.username) console.log(currentuser.username + " viewing Account Info"); res.render('users/userconfig', { sitename: sitename, currentuser: currentuser, useraccount: useraccount, }) await sessionstore.renewsession(req.cookies.sessionid) }else{ res.render('status/unauthorized', { title: "Unauthorized" }) } }else{ res.render('status/notsignedin', { sitename: sitename, title: sitename }) } });
router.get('/attachtwitter', async (req, res) => { var sessionid = req.cookies.sessionid var currentuser = await sessionstore.getsession(sessionid); if (currentuser){ if (currentuser.securitylevel >= 50){ var authLink = await twitterapi.genlinkv1(currentuser); console.log(currentuser.username + " Starting Twitter Account Connection"); res.render('users/attachtwitter', { sitename: sitename, currentuser: currentuser, authLink: authLink, }) await sessionstore.renewsession(req.cookies.sessionid) }else{ res.render('status/unauthorized', { title: "Unauthorized" }) } }else{ res.render('status/notsignedin', { sitename: sitename, title: sitename }) } });
router.get('/checktwitter', async (req, res) => { var sessionid = req.cookies.sessionid var currentuser = await sessionstore.getsession(sessionid); if (currentuser){ if (currentuser.securitylevel >= 50){ var useraccount = await useraccounts.getbyusername(currentuser.username) var tsettings = await twitterapi.gettwittersettings(useraccount); console.log(currentuser.username + " Starting Twitter Account Connection"); res.render('users/checktwitter', { sitename: sitename, currentuser: currentuser, tsettings: tsettings, }) await sessionstore.renewsession(req.cookies.sessionid) }else{ res.render('status/unauthorized', { title: "Unauthorized" }) } }else{ res.render('status/notsignedin', { sitename: sitename, title: sitename }) } });
router.get('/twitauthcallback', async (req, res) => { var {oauth_verifier} = req.query var {oauth_token} = req.query console.log(oauth_verifier) console.log(oauth_token) var sessionid = req.cookies.sessionid var currentuser = await sessionstore.getsession(sessionid); if (currentuser){ if (currentuser.securitylevel >= 50){ var useraccount = await useraccounts.getbyusername(currentuser.username) if (oauth_verifier){ console.log("Received PIN for twitter attachment \n") var errormessages = await twitterapi.gentokenv1(useraccount, oauth_verifier) }else{ var errormessages = [] errormessages.push("Invalid PIN entry") console.log("Invalid PIN entry") } if (errormessages.length > 0){ console.log(errormessages) var authLink = await twitterapi.genlinkv1(currentuser); console.log(currentuser.username + " Starting Twitter Account Connection"); res.render('users/attachtwitter', { sitename: sitename, currentuser: currentuser, authLink: authLink, errors: errormessages, }) await sessionstore.renewsession(req.cookies.sessionid) }else{ res.redirect('/user/settings') } }else{ res.render('status/unauthorized', { title: "Unauthorized" }) }
}else{ res.render('status/notsignedin', { sitename: sitename, title: sitename }) } });
module.exports = router;
|
Notes: The above code references my own session store and user security settings, feel free to replace with your own or omit as neccessary.
Views
To start attachment of account - Attachtwitter.pug
extends layout
block content -data = data || {} -pagetitle = "Attach Twitter Account"
if errors ul.my-errors for error in errors li= error if mainerror br .text-danger ul.my-errors2 li= mainerror
.row .col-2 .col-8 br .card .card-body .row .col-2 .col-8 br center h1 = boardname br = pagetitle .col-2 br br a(class='button btn btn-lg btn-outline-danger', href='/user/settings') Back br br br br .row .col-1 .col-10 center if authLink if authLink.url h5 |Follow this link to login and authorize twitter: br a(class='button btn btn-info', href=authLink.url) |Twitter br br else h5 |Unspecified additional error encountered br br else h5 |Unspecified additional error encountered br br
|
Page view to check account connection - checktwitter.pug
extends layout
block content -data = data || {} -pagetitle = "Twitter Account" if oauth_verifier -data.oauth_verifier = oauth_verifier
if errors ul.my-errors for error in errors li= error.msg if mainerror br .text-danger ul.my-errors2 li= mainerror
.row .col-2 .col-8 br .card .card-body .row .col-2 .col-8 br center h1 = boardname br = pagetitle .col-2 br br a(class='button btn btn-lg btn-outline-danger', href='/user/settings') Back br br br br .row .col-4 .col-4 center if tsettings if tsettings.screen_name h5 |Attached Account: |@ =tsettings.screen_name br |Account connected OK! br br else h5 |Attached Account: |Error! Please reconnect Account! br br a(class='button btn btn-lg btn-outline-info', href='/user/attachtwitter') Connect br br else h5 |Attached Account: |Error! Please reconnect Account! br br a(class='button btn btn-lg btn-outline-info', href='/user/attachtwitter') Connect br br
|
How does it all fit together and work?
Ok, so the above flow works as follows:
1 - The user opens the attachtwitter link from the user settings page
2 - The geninkv1 function in our twitterapi module is called before rendering the page. This function sends a request to twitter with your app token and secret, and references your desired callback url. This returns a unique url that we want to send the user to to authorize our app to make submissions, and an authorization token and secret for the user account. The latter information is saved to the user record.
3 - The user clicks the link on the attachment page and gets sent to twitter where they will login and grant authorization to our app
4 - The user is redirected to our callback url
5 - Our routes file extracts the verification token twitter attaches to the callback url
6 - gentokenv1 is called and sends a request to twitter containing our app key and secret, and the user account authorization token and secret, and the validation pin sent by twitter. The request returns the final needed access token and secret, which are then stored to the user account record.
7 - Our app tests connection to the twitter user acount, and saves the screenname to our local user record.
At this point, we're all setup and authorized, and we can tweet by passing the useraccount and a tweet message to sendusertweet() in our twitterapi module. Easy peasy!
I setup a timed event that runs a function that sends newly published material on my blog to twitter, but you can do or build whatever you please with this new knowledge!