Search Icon, Magnifying Glass

Marmanold.com

Graduation Cap Heart Question Mark Magnifying Glass

Automated *Pretty* Tweets to DayOne

I’ve been using DayOne as my journal since 2014. Very early on, I realized that a good part of my daily journaling was actually done on Twitter. Over the years I’ve used IFTTT to import my tweets, but the fact that that system didn’t include images or quoted tweets removed a lot of important context. I wanted something nicer that showed the full Twitter card with graphics, etc. DayOne’s recent release of activity feeds seemed to be a good solution, but it, too, lost a lot of context and was very manual.

This weekend I finally got around to solving my problem by creating a series of scripts and automations that pull down my tweets, generate a pretty Twitter card image of the tweet with context, and adds that tweet image automatically to DayOne. Let me step you through my process.

Grab Recent Tweets

To get my Tweets I use Node.js and the Twitter Javascript API. I have a little properties file (props.json) to keep all my API keys and a file to simply keep track of the last Tweet ID I’ve generated an image for (lastTweet.json).

As I loop through each of my tweets, I hit Twitter’s OEmbed end point to get the HTML needed for an embeddable Twitter card. I insert that code into a simple HTML file that I save to the disk.

get_tweets.js
"use strict";

const fetch = require('node-fetch');

const Twitter = require('twitter');
const fs = require('fs');

const propsFile = JSON.parse(fs.readFileSync('./props.json', 'utf8'));
let lastTweet = JSON.parse(fs.readFileSync('./lastTweet.json', 'utf8'));

let client = new Twitter({
  consumer_key: propsFile.consumer_key,
  consumer_secret: propsFile.consumer_secret,
  access_token_key: propsFile.access_token_key,
  access_token_secret: propsFile.access_token_secret
});

let params = {
    screen_name: propsFile.username,
    include_rts: false, 
    trim_user: true, 
    since_id: lastTweet.last_id
};

client.get('statuses/user_timeline', params, function(error, tweets, response) {
  if (!error) {
    tweets.forEach(function(tweet) {
        fetch(`https://publish.twitter.com/oembed?url=https://twitter.com/itdoesnotmatter/status/${tweet.id_str}&omit_script=true&maxwidth=550`).then(function(response){
            return response.json();
        }).then(function(json) {
            fs.writeFileSync(`tmp/${tweet.id_str}.html`, `<html><head><style>body{background-color:white;}</style></head><body>${json.html}</body></html>`, 'utf8');

            if (tweet.id > parseInt(lastTweet.last_id)) {
                lastTweet.last_id = tweet.id_str;
                fs.writeFileSync('./lastTweet.json', JSON.stringify(lastTweet), 'utf8');
            }
        });
    });
  }
});

Render an Image for each Tweet

Now that I have the HTML page for a pretty Twitter card, I only need to capture that page as an image file using Phantomjs — png in this examle —, remove additional white space using ImageMagick — Twitter gives a guaranteed width for the card, but not a length so there’s a lot of white space at the bottom of my view port —, and copy the final image out to a processing directory.

render_tweets.js
"use strict";

const exec = require('child_process').execSync;
const fs = require('fs');

fs.readdir('tmp/',function(err,files){
    if(err) throw err;
    files.forEach(function(file){
        exec(`/usr/local/bin/phantomjs /Users/marnold/code/tweetToImage/src/phantom_render.js tmp/${file} tmp/${file}`, (error, stdout, stderr) => {
          if (error) {
            console.error(`exec error: ${error}`);
            return;
          }
        });

        exec(`convert tmp/${file}.jpeg -trim ~/Dropbox/Apps/tweetToImage/${file}.jpeg`, (error, stdout, stderr) => {
          if (error) {
            console.error(`exec error: ${error}`);
            return;
          }
        });

    });
 });
phantom_render.js
"use strict";

var page = require('webpage').create(), 
    system = require('system'), 
    address, 
    image_name;

address = system.args[1];
image_name = system.args[2];
page.viewportSize = { width: 550, height: 1024 };
page.clipRect = { top: 0, left: 0, width: 550, height: 1024 };
page.settings.XSSAuditingEnabled = false;
page.settings.webSecurityEnabled = false;
page.settings.resourceTimeout = '1000';

page.open(address, function(status) {
    if(status !== 'success') {
        console.log('Unable to load: ' + address);
        phantom.exit(1);
    }
    else {
        page.includeJs('http://platform.twitter.com/widgets.js', function() {
            window.setTimeout(function() {
                page.render(image_name + '.jpeg', {format:'jpeg', quality:'100'});
                phantom.exit();
            }, 1000);
        });
    }
  
});

Add to DayOne via the CLI and Hazel

DayOne provides a command line interface (cli) on the Macintosh that allows you do add DayOne entries, including images, to a journal. Using Hazel, I monitor the processing directory my Node script drops the photos in. As each image of a Tweet arrives, Hazel runs the DayOne cli and adds a new journal entry with my tweet. After a week, Hazel will automatically move the image to the Trash to free up space.

DayOne CLI Script
dayone2 new -p $1 -t Twitter

Keep on Truckin'

This process works all fine and well so long as I remember to run my Node script each time after I tweet. To keep things automated, I added a further tool to the workflow called Lingon. Lingon is a UX to the launchd scheduling service on macOS. I could have manually modified property files to do all of this, but the Lingon UX makes things easier to test and less error prone. I configured Lingon to run my little Node script every five minutes to look for a new tweet and kick the whole process off again.

Conclusion

Is this a little overwrought to get my tweets into DayOne? Probably. But, the end result looks very nice.