Why and how I delete my tweets after 10 days

I thought I was releasing my flighty words into the world with every tweet, but in reality, Twitter was just a bird cage rapidly filling with every inane utterance I flung into it.

Source: https://media.giphy.com/media/3o6ZsUdQYvPAyT1hxm/giphy.gif

[UPDATE: Since writing this tutorial, I heavily modified the scripts and moved the hosting over to Heroku to schedule it. You can find the updated code at https://github.com/JacksonBates/tweet-delete-bot]

Why do I do it?

It started when a previous Medium post I published with freeCodeCamp got some attention and plenty of likes and shares.

After the initial buzz died down, I noticed that on a weekly basis there was one user that kept sharing a tweet about it — same format each time — and only a handful of other tweets in his timeline. There was some mention in his bio about ‘ephemeral Twitter’ and I realised he was deleting old tweets automatically, and must also have a bot re-posting previous tweets if there was no new content to replace the old.

Why would anyone do this? Well, he wasn’t alone, and once I was introduced to the idea, it kept popping up as a compelling solution to some of the things that make me uneasy about Twitter.

The sometimes irreverent, usually argumentative, and often silly way in which I currently engage on Twitter doesn’t really serve any long-term goals of mine. The idea that all my nonsense sticks around for any of my students, their parents, or other interested parties to delve into was a little troubling.

My primary reason for regularly purging the historical record of my tweets, though, is that I am regularly finessing my beliefs and opinions — sometimes changing my mind about issues completely.

‘140 characters’ does not really do finesse, though.

Source: https://media.giphy.com/media/MkJyqTHzKGOjK/giphy.gif

Those old tweets just sit there reminding me of beliefs I held long ago, and usually feel like I’ve progressed away from. Keeping them around serves no purpose.

I like the idea that Twitter is conversational and current. I feel like Twitter is supposed to be ephemeral, but in reality it is not.

So, I wrote a node bot that deletes all tweets and unlikes any favourites that are more than 10 days old. I scheduled this on my Windows machine to run once a day. If I don’t use my computer that day it’s no big deal, but it automates the process without me having to think about it, or rent a server for this tiny task.

How do I do it?

The short answer is: standing on the shoulders of giants.

Most of the code for the script is based on some of the suggestions from this previous freeCodeCamp article, by Scott Spence: How to build and deploy a multifunctional Twitter bot and the example code provided in the Twit NPM package.

I decided to use Windows Task Scheduler to run the script instead of hosting it on a remote server. I made some modifications to the advice found in Edd Yerburgh’s blog post: Run a Node Script With Windows Task Scheduler. When I hand the work computer back, this will probably get turned into a cronjob on my Linux machine at home.

Step One

You need to set up a Twitter App first, and get the relevant keys. Visit https://apps.twitter.com/app/new to get started.

You will need the Consumer Key (API Key), Consumer Secret (API Secret), Access Token, and Access Secret. We’ll store these keys in a .env file later, in case we want to store the code on Github without making our keys public.

Step Two

I used Git-bash on Windows and obviously also needed Node and npm. If you don’t have them installed, take care of that next:

Test that Node and npm are working correctly in Git-bash by typing and running these commands at the prompt:

> node -v
> npm -v

In each case, you should see a version number. You may need to restart Windows after installing them all before Git-bash can run Node and npm.

Step Three

We’ll borrow Scott Spence’s app architecture and set-up from the blog post linked to above, and create the necessary files for our app.

Run the following commands at the Git-bash prompt:

> mkdir ephemeral-twitter
> cd ephemeral-twitter
> npm init
> npm install --save twit dotenv
> touch .env .gitignore index.js

These commands 1) make a new directory, 2) change to that directory, 3) initialises a new npm package, 4) installs and saves the twit and dotenv packages to the package.json, and 5) creates 3 blank files: .env (for our API keys), .gitignore (for the files we want to keep off a remote public git repo), and index.js, our entry script file.

We’ll also add a script to the package.json to make running things a little easier:

"scripts": {
"start": "node index.js"
},

So your completed package.json should look something like this:

{
"name": "ephemeral-twitter",
"version": "1.0.0",
"description": "Deletes and unlikes older tweets",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "JacksonBates",
"license": "GPL-3.0",
"dependencies": {
"dotenv": "^4.0.0",
"twit": "^2.2.9"
}
}

Now add the following to the index.js file:

require('./src/bot')

All this does is point to the bot script in another folder. Is this overkill for such a simple, small bot? Kinda. But we’re sticking with Scott’s original style for consistency.

Then add the API keys to the .env file:

CONSUMER_KEY=<insert key here>
CONSUMER_SECRET=<insert key here>
ACCESS_TOKEN=<insert key here>
ACCESS_TOKEN_SECRET=<insert key here>

<insert key here> is where your keys go.

Also, add node_modules and .env to your .gitignore file.

Finally, Scott gets us to create our config.js and bot.js files.

At the Git-bash prompt:

mkdir src
cd src
touch config.js bot.js

Add the following to the config.js file:

require('dotenv').config()

module.exports = {
consumer_key: process.env.CONSUMER_KEY,
consumer_secret: process.env.CONSUMER_SECRET,
access_token: process.env.ACCESS_TOKEN,
access_token_secret: process.env.ACCESS_TOKEN_SECRET,
}

This essentially reads the API keys from the .env file and makes them available within your code, without exposing them on Github or other public repos. You could have set these as environment variables instead, and saved an additional node_module…but dotenv is lightweight, and it’s less fiddly to use.

The final part of our set-up is to add the following to bot.js:

const Twit = require('twit')
const config = require('./config')

const bot = new Twit(config)

Test your progress so far with npm start at the Git-bash prompt. If you get no error messages, you’re good to continue.

If you want more of a ‘hello, world’ example, continue reading Scott’s post. He suggests some interesting things to do with your bots, so it’s worth diving into the rest of his article.

Step 4

Adding the tweet-killers!

Source: https://media.giphy.com/media/CNna9G8BQvnoI/giphy.gif

Here’s the rest of the code that goes in bot.js:

bot.get('statuses/user_timeline', {
count: 200
}, (err, data, response) => {
if (err) {
console.log(err)
} else {
var now = Date.now();
var ten_days_ago = now - 864000000;
for (var i = 0; i < data.length; i++) {
if ( Date.parse(data[i].created_at) < ten_days_ago ) {
bot.post('statuses/destroy/:id', {
id: data[i].id_str.toString()
}, (err, data, response) => {
if (err) {
console.log(err)
} else {
console.log(`${data.text} tweet deleted!`)
}})
}
}
}
})
bot.get('favorites/list', {
screen_name: 'jacksonbates',
count: 200
}, (err, data, response) => {
if (err) {
console.log(err);
} else {
var now = Date.now();
var ten_days_ago = now - 864000000
for (var i = 0; i < data.length; i++) {
if ( Date.parse(data[i].created_at) < ten_days_ago ) {
bot.post('favorites/destroy', {
id: data[i].id_str.toString()
}, (err, data, response) => {
if (err) {
console.log(err)
} else {
console.log(`${data.text} tweet unliked!`)
}
})
}
}
}
}
)

It’s not as clean as I’d like, but it gets the job done. Feel free to suggest more sensible approaches.

So what does it all mean?

Essentially we have two primary Twitter API GET calls, handled by the npm package twit:

bot.get('statuses/user_timeline'...)
// and //
bot.get('favorites/list'...)

These should be straightforward enough. They are the API endpoints for your own timeline and your list of ‘liked’ tweets.

I’ll deconstruct each in more detail, in turn.

bot.get('statuses/user_timeline', {
count: 200
}, (err, data, response) => {
if (err) {
console.log(err)
} else {
console.log(data)
}

Above is a simplified API call using twit. It gets 200 tweets from the user timeline (the maximum allowed by Twitter) and returns err, data, and response objects.

We use an arrow function to deal with the returned objects: if there’s an error, we log the error, if not, we log the data. The data is an array of 200 JSON objects detailing the tweets. I recommend running the above call (maybe with a count: 1) to familiarise yourself with the structure of Tweet objects.

Simply logging the data is not that useful though, so our bot will do something with the data.

First, we determine which ones are more than 10 days old. 10 days is 864000000 ms so we can subtract that from Date.now() to get the timestamps we need to filter out old tweets.

We can access the date each tweet was created with the created_at attribute in the JSON objects in our data array. Because Twitter stores this time as a date string, rather than unix timestamp, we can use Date.parse() to workout the difference:

var now = Date.now();
var ten_days_ago = now - 864000000;
for (var i = 0; i < data.length; i++) {
if ( Date.parse(data[i].created_at) < ten_days_ago ) {
...

As you can see above, we use a for loop here to check each item in the array of 200 returned JSON objects. If the current tweet in the loop is older than 10 days, we execute the delete command:

bot.post('statuses/destroy/:id', {
id: data[i].id_str.toString()
}, (err, data, response) => {
if (err) {
console.log(err)
} else {
console.log(`${data.text} tweet deleted!`)
}
})

This should look familiar, as it is similar to the GET calls we encountered earlier. This POST request takes the tweet id string, and if it is successfully deleted, logs a success message for us.

I worried that running this without any rate limiting to cause issues, but I haven’t had any problems yet.

The unliking favorites method is almost identical. We start with the same loop that filters out the older tweets, and then we use a different, but by now familiar POST method to get the job done:

bot.post('favorites/destroy', {
id: data[i].id_str.toString()
}, (err, data, response) => {
if (err) {
console.log(err)
} else {
console.log(`${data.text} tweet unliked!`)
}
})

For my initial clean up, I could only deal with 200 tweets at a time, so I just manually ran the script until I deleted all the offending tweets.

Type npm start from your ephemeral-twitter directory, and repeat as needed. Watch the tweets melt away by the hundreds. I deleted 1400 tweets, and unliked 700 in less than a minute.

Source: https://media.giphy.com/media/1hiVNxD34TpC0/giphy.gif

It felt real good.

Step 5

Using Windows Task Scheduler ended up being a little clunky, but I got it working in the end. Follow all of Edd’s advice here: https://eddyerburgh.me/run-a-node-script-with-windows-task-scheduler

But when you get to the bit about including arguments for Git-bash in the action window, use the following:

-l -i -c "cd ~/ephemeral-twitter;npm start"

This replaces Edd’s advice to use:

--a -i -c "cd ephemeral-twitter;npm start"

I don’t know why Edd’s needing tweaking, but if his way doesn’t work on your machine, hopefully mine will.

Step 6

Set it and forget it!

You’ve gotta dance like there’s nobody watching,
Love like you’ll never be hurt,
Sing like there’s nobody listening,
And tweet it like you’re gonna delete it.

Source: https://media.giphy.com/media/bNkbya2yDAz7O/giphy.gif

Thanks for taking the time to read :)