Node.JS Programming
June 08, 2022
Send Tweets From NodeJS and Express
by: Underminer

For when you want the twit to hit the fan


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!