JeanCarl's Adventures

Project 6: Prompt Me with Evernote

June 22, 2015 |

When I was a young lad, I got really interested in writing short stories. I still have binders of short stories and newsletters I wrote when I was inspired by R.L. Stine’s Goosebumps series. In fact, I enjoyed writing more than reading the books. It was about the same time when I started developing websites and learning Perl. Yes, my first website did in fact publicize my upcoming short stories. Every pre-teen has to start somewhere. That’s a story for another time.

At times, as most writers have probably experienced, it was a challenge to think up a topic to write about. I wrote a simple Perl script to compose a writing prompt each day. It would randomly pick a couple of words out of a list and save them into a text file. The challenge was to then write a couple of paragraphs using these words. It didn’t matter where the story went, or what it was about. Just that I wrote about something using those selected words.

This repetitive practice developed a useful skill of being able to write about random topics (some of the words and objects had no commonality, which added an additional challenge). In doing so, I learned to compose my thoughts and write them down in a way that made a random short story make sense.

To give you an idea of what a prompt would have looked like, and the story, here’s an example:

Use these words: balloon, rabbit, Bobby, jump, gold

Bobby was a short little kid for his age. As he walked down the street to the bus stop, he was startled when a rabbit jumped out of the bushes. He was so surprised that his hand let go of the string he was holding. Bobby was as scared, if not more scared, as the rabbit must have been. After a brief second, he realized he had let go of the string attached to his balloon! Fortunately, the balloon didn’t fly away since his lucky gold nugget was attached to the end of the string.

He heard the bus powering over the hilltop and watched as it came to a stop in front of him. He pulled out his bus pass and stepped onboard.

“Hey Bobby! Where are you going today?” the bus driver asked her favorite rider. She already knew the answer.

In this sixth project of my 15 projects in 30 days, I’m going to write a similar app in AngularJS and Node.js. I’m going to use Evernote’s API to save the prompt as a note in an Evernote notebook. I could write an editor, but Evernote has that part covered.

Evernote

Evernote is great at storing and organizing content. In Prompt Me, a user will authenticate with Evernote via OAuth, giving Prompt Me access to the notebooks the user has. At some time in the future which the user has specified, Prompt Me will generate a prompt and create a note in the selected Evernote notebook.

Prompt Me will need an Evernote API key. Sign up for a Evernote API Key.

Photo

Copy Consumer Key and Consumer Secret for later.

We’re going to use the Evernote Sandbox, a separate site from the production Evernote service. This way we can create accounts, play around with things, and not be worried about messing with a real account.

To access the sandbox, use this URL: https://sandbox.evernote.com. Sign up with a new account and create a couple of notebooks.

Photo

MongoDB

We’ll use MongoDB to store the subscriptions.

To install MongoDB, run this command:

apt-get install mongodb

To check that it is running, run this command:

service mongodb status

Node.js

To install Node.js:

sudo apt-get update
sudo apt-get install nodejs
sudo apt-get install npm

Some version info:

# nodejs -v
v0.10.25

# npm -v
1.3.10

Setup

There are six files for this project. app.js and makenotes.js are the Node.js apps, the former being the app that will create, manage subscriptions and serve up the website for Prompt Me, the latter looks for subscriptions to process in the MongoDB database and create notes in Evernote notebooks. These two files share config.json where common settings are located. And package.json is for the npm install command to install the necessary dependencies. For the front end, there’s the AngularJS controller, promptme.js, and view, index.html, both located in the public folder.

// Filename: app.js

var express = require('express');
var http = require('http');
var bodyParser = require('body-parser');
var app = express();
var Evernote = require('evernote').Evernote;
var config = require('./config.json');
var mongoose = require('mongoose');

app.use(express.cookieParser('secret'));
app.use(express.session());
app.use(bodyParser.json());
app.use(express.static(__dirname + '/public'));

mongoose.connect(config.MONGODB_ADDRESS);

var Subscription = mongoose.model(config.MONGODB_MODEL, {
  userId: String,
  nextPrompt: Number,
  frequency: Number,
  token: String,
  tokenExpiration: Number,
  noteStore: String,
  notebookId: String,
});

app.get('/oauth', function(req, res) {
  var client = new Evernote.Client({
    consumerKey: config.API_CONSUMER_KEY,
    consumerSecret: config.API_CONSUMER_SECRET,
    sandbox: config.SANDBOX
  });

  client.getRequestToken(config.CALLBACK_URL, function(error, oauthToken, oauthTokenSecret, results) {
    if(error) {
      req.session.error = JSON.stringify(error);

      res.redirect('/');
    } else { 
      req.session.oauthToken = oauthToken;
      req.session.oauthTokenSecret = oauthTokenSecret;

      res.redirect(client.getAuthorizeUrl(oauthToken));
    }
  });
});

app.get('/oauth_callback', function(req, res) {
  var client = new Evernote.Client({
    consumerKey: config.API_CONSUMER_KEY,
    consumerSecret: config.API_CONSUMER_SECRET,
    sandbox: config.SANDBOX
  });

  client.getAccessToken(
    req.session.oauthToken, 
    req.session.oauthTokenSecret, 
    req.param('oauth_verifier'), 
    function(error, oauthAccessToken, oauthAccessTokenSecret, results) {
      if(error) {
        console.log('error');
        console.log(error);
        res.redirect('/');
      } else {
        // store the access token in the session
        req.session.oauthAccessToken = oauthAccessToken;
        req.session.oauthAccessTtokenSecret = oauthAccessTokenSecret;
        req.session.edamShard = results.edam_shard;
        req.session.edamUserId = results.edam_userId;
        req.session.edamExpires = results.edam_expires;
        req.session.edamNoteStoreUrl = results.edam_noteStoreUrl;
        req.session.edamWebApiUrlPrefix = results.edam_webApiUrlPrefix;
        res.redirect('/');
      }
    });
});

app.get('/api/logout', function(req, res) {
  req.session.destroy();
});

app.get('/api/notebooks', function(req, res)
{
  if(!req.session.edamUserId)
    return res.send(401, {error:'Not logged in'});

  var client = new Evernote.Client({
    consumerKey: config.API_CONSUMER_KEY,
    consumerSecret: config.API_CONSUMER_SECRET,
    sandbox: config.SANDBOX,
    token:req.session.oauthAccessToken
  });

  var noteStore = client.getNoteStore(req.session.edamNoteStoreUrl);
  var response = [];
  
  var notebooks = noteStore.listNotebooks(function(err, notebooks) {
    for(var i in notebooks) {
      response.push({
        'notebookid': notebooks[i].guid, 
        'title': notebooks[i].name
      });
    }

    res.setHeader('Content-Type', 'application/json');
    res.send(JSON.stringify(response));
  });
});

app.get('/api/subscriptions', function(req, res) {
  if(!req.session.edamUserId)
    return res.send(401, {error:'Not logged in'});

  Subscription.find({'userId': req.session.edamUserId}, function(err, docs) {
    var results = [];

    for(var i in docs) {
      results.push({subscriptionid: docs[i]._id, notebookid:docs[i].notebookId});
    }

    res.setHeader('Content-Type', 'application/json');
    res.send(JSON.stringify(results));
  });
});

app.post('/api/subscribe', function(req, res) {
  if(!req.session.edamUserId)
    return res.send(401, {error:'Not logged in'});

  Subscription.create({
    userId: req.session.edamUserId,
    nextPrompt: req.body.start,
    frequency: req.body.frequency,
    token: req.session.oauthAccessToken,
    tokenExpiration: req.session.edamExpires,
    noteStore: req.session.edamNoteStoreUrl,
    notebookId: req.body.notebookid
  }, function(err, doc) {
      if(err) 
        return res.send(500, {error:err});

      return res.send(JSON.stringify({
        subscriptionid: doc._id, 
        notebookid:doc.notebookId
      }));
  });
});

app.post('/api/unsubscribe', function(req, res) {
  if(!req.session.edamUserId)
    return res.send(401, {error:'Not logged in'});

  Subscription.remove({_id: req.body.subscriptionid}, function(err, doc) {
    res.setHeader('Content-Type', 'application/json');
    res.send(JSON.stringify({subscriptionid: req.body.subscriptionid}));
  });
});

app.listen(config.PORT);

console.log('Application listening on port '+config.PORT);
// Filename: makenotes.js

var Evernote = require('evernote').Evernote;
var config = require('./config.json');

function generatePrompt()
{
  var names = [['Jenny','her'],['Melissa','her'],['Billy','his'],['Johnny','his'],['Alexis','her']];

  var objects = ['dog','cat','balloon','new car'];

  var actions = ['lost','visited','played with'];

  var places = ['at the park','at home','at school','on the bus'];

  var randomName = names[Math.floor(Math.random()*names.length)];
  var randomObject = objects[Math.floor(Math.random()*objects.length)];
  var randomAction = actions[Math.floor(Math.random()*actions.length)];
  var randomPlace = places[Math.floor(Math.random()*places.length)];

  return {
    title: randomName[0]+' and '+randomName[1]+' '+randomObject,
    prompt: randomName[0]+' has a '+randomObject+'. Write about the time when '+randomName[0]+' '+randomAction+' '+randomName[1]+' '+randomObject+' '+randomPlace+'.'
  }
}

function makeNote(noteStore, noteTitle, noteBody, parentNotebookId, callback) {
  var nBody = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
  nBody += "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">";
  nBody += "<en-note>" + noteBody + "</en-note>";
 
  var ourNote = new Evernote.Note();
  ourNote.title = noteTitle;
  ourNote.content = nBody;
  ourNote.notebookGuid = parentNotebookId;
 
  noteStore.createNote(ourNote, function(err, note) {
    if(err) {
      console.log(err);
    } else {
      callback(note);
    }
  });
}

var mongoose = require('mongoose');
var mongooseServerAddress = config.MONGODB_ADDRESS;

mongoose.connect(mongooseServerAddress);

var Subscription = mongoose.model(config.MONGODB_MODEL, {
  userId: String,
  nextPrompt: Number,
  frequency: Number,
  token: String,
  tokenExpiration: Number,
  noteStore: String,
  notebookId: String,
});

setInterval(function() {
  var timeNow = new Date();
  Subscription.find().where('nextPrompt').gt(0).lt(timeNow.getTime()).where('tokenExpiration').gt(timeNow.getTime()).exec(function(err, results) {
    for(var i=0; i<results.length; i++) {
      var sub = results[i];

      var client = new Evernote.Client({
        consumerKey: config.API_CONSUMER_KEY,
        consumerSecret: config.API_CONSUMER_SECRET,
        sandbox: config.SANDBOX,
        token: sub.token
      });

      var noteStore = client.getNoteStore(sub.noteStore);

      var randomPrompt = generatePrompt();

      makeNote(noteStore, randomPrompt.title, randomPrompt.prompt, sub.notebookId, function(note) {
        Subscription.update({_id:sub._id}, {nextPrompt:timeNow.getTime()+sub.frequency});
      });
    }
  });
}, config.SERVER_CHECK_FREQUENCY);
// Filename: config.json
{
  "API_CONSUMER_KEY" : "EVERNOTEAPICONSUMERKEY",
  "API_CONSUMER_SECRET" : "EVERNOTEAPICONSUMERSECRET",
  "SANDBOX" : true,
  "CALLBACK_URL" : "http://IPADDRESS:PORT/oauth_callback",
  "MONGODB_ADDRESS" : "mongodb://127.0.0.1:27017/test",
  "MONGODB_MODEL" : "Evernote",
  "SERVER_CHECK_FREQUENCY" : 60000,
  "PORT": 3000
}
// Filename: package.json
{
  "name": "prompt-me",
  "description": "Prompt Me app for Node.js",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "nodejs app"
  },
  "dependencies": {
    "express": "3.1.0",
    "less-middleware": "*",
    "evernote": "*",
    "body-parser": "*"
  }
}
// public/promptme.js

var app = angular.module('promptMeApp', []);

app.factory('promptMeAPI', ['$http', function($http) {
  return {
    getNotebooks: function() {
        return $http.get('/api/notebooks');
    },
    subscribe: function(options) {
      return $http.post('/api/subscribe', options);
    },
    getSubscriptions: function() {
      return $http.get('/api/subscriptions');
    },
    unsubscribe: function(subscription) {
      return $http.post('/api/unsubscribe', {subscriptionid: subscription.subscriptionid});
    }
  }
}]);

app.controller('promptMeCtrl', ['$scope', '$filter', 'promptMeAPI', function($scope, $filter, promptMeAPI) {
  $scope.notebooks = [];
  $scope.selectedNotebook = '';
  $scope.frequencies = [{interval:24*60*60,label:'Daily'}, {interval:48*60*60,label:'Every other day'}]
  $scope.selectedFrequency = 24*60*60;
  $scope.loggedIn = false;
  $scope.startDate = new Date();
    $scope.startTime = new Date();
    $scope.startTime.setMilliseconds(0);
    $scope.startTime.setSeconds(0);
    $scope.subscriptions = [];

  promptMeAPI.getNotebooks().success(function(results) {
    $scope.notebooks = results;
    $scope.loggedIn = true;
    $scope.selectedNotebook = $scope.notebooks[0].notebookid;
  });

  promptMeAPI.getSubscriptions().success(function(subscriptions) {
    $scope.subscriptions = subscriptions;
  });

  $scope.getNotebookTitle = function(notebookId) {
    for(var i=0; i<$scope.notebooks.length; i++) {
      if($scope.notebooks[i].notebookid == notebookId) {
        return $scope.notebooks[i].title;
      }
    }

    return '';
  }

  $scope.unsubscribe = function(subscription) {
    promptMeAPI.unsubscribe(subscription).success(function(subscription) {
      var oldSubscriptions = $scope.subscriptions;

      $scope.subscriptions = [];
      for(var i=0; i<oldSubscriptions.length; i++) {
        if(oldSubscriptions[i].subscriptionid != subscription.subscriptionid) {
          $scope.subscriptions.push(oldSubscriptions[i]);
        }
      }      
    });
  }

  $scope.subscribe = function() {
    var nextPrompt = new Date($filter('date')($scope.startDate, 'yyyy-MM-dd')+' '+$filter('date')($scope.startTime, 'HH:mm'));

    var options = {
      notebookid: $scope.selectedNotebook,
      start: nextPrompt.getTime(),
      frequency: parseInt($scope.selectedFrequency)
    };

    promptMeAPI.subscribe(options).success(function(subscription) {
      $scope.subscriptions.push(subscription);
    });
  }
}]);
<!-- Filename: public/index.html -->
<!DOCTYPE html> 
<html ng-app="promptMeApp">
	<head>
		<meta charset="utf-8">
		<title>Prompt Me!</title> 
		<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.16/angular.min.js"></script>
		<script src="promptme.js"></script>
	</head> 
	<body ng-controller="promptMeCtrl" style="font-family:Arial"> 
		<h2>Prompt Me!</h2>
		<a href="/oauth" ng-hide="loggedIn" style="text-decoration:none">Connect with<br />
		<img src="https://evernote.com/media/img/logos/evernote_logo_4c-sm.png" /></a>

		<div ng-show="loggedIn">
			Add a new prompt to 
			<select ng-model="selectedNotebook">
				<option ng-repeat="notebook in notebooks" value="{{notebook.notebookid}}">{{notebook.title}}</option>
			</select>
			<select ng-model="selectedFrequency">
				<option ng-repeat="frequency in frequencies" value="{{frequency.interval}}">{{frequency.label}}</option>
			</select>	
			starting on
			<input type="date" ng-model="startDate" />
			at
			<input type="time" ng-model="startTime"/>
			<input type="button" ng-click="subscribe()" value="Subscribe" />
			
			<h2>Subscriptions</h2>
			<div ng-show="subscriptions.length == 0">
				You don't have any subscriptions right now!
			</div>
			<div ng-repeat="subscription in subscriptions">
				<input type="button" ng-click="unsubscribe(subscription)" value="Unsubscribe" /> {{getNotebookTitle(subscription.notebookid)}}
			</div>
		</div>
	</body>
</html>

In config.json, replace EVERNOTEAPICONSUMERKEY, EVERNOTEAPICONSUMERSECRET with the Consumer Key and Consumer Secret values provided in the Evernote section. Replace IPADDRESS and PORT with the IP address and port of the app.js Node.js app.

To install the dependencies, run the following command:

npm install

And start the two Node.js apps (you’ll need two terminals):

nodejs app.js
nodejs makenotes.js

Prompt Me

Prompt Me is pretty simple for now. Point your browser to http://IPADDRESS:PORT/ where IPADDRESS is the ip address and PORT is the port where the Node.js app is running. You should see this:

Photo

Click on the Evernote logo to connect your sandbox account with Prompt Me. Evernote will confirm that you’re authorizing the app to access the notebooks in the sandbox account.

Photo

After clicking Authorize, Evernote will send back an OAuth token to the app, which will be saved in the user session. The AngularJS app will make two calls to the Node.js app, to get notebooks and subscriptions, for the current user.

Photo

The first drop down menu will show all the notebooks that are in this Evernote user’s account. The second drop down gives different frequencies for the prompts to be added to the account. And the date and time the first prompt should be added, with subsequent prompts at said frequency.

When the user clicks on Subscribe, our AngularJS app will make an API call to the Node.js app, which will add a subscription to the MongoDB. The subscription is added to a list below, which offers a way to Unsubscribe. A user can come back to this page and see their subscriptions at anytime, and unsubscribe as needed.

Photo

The server.js Node.js application will check every 60000 milliseconds, or every minute, and look for subscriptions that have the nextPrompt value less than the current time, or before “now”. That means we should process those subscriptions. The generatePrompt in server.js will randomly pick values and compose a prompt. The makeNote function will make a request to the Evernote API to add a note with our prompt to the selected notebook.

After the specified time (which could take up to the interval longer), refresh the Evernote notebook on http://sandbox.evernote.com. You should see a new note with the prompt.

Photo

Excuse me while I write a little bit about Jenny and her balloon at the park. It’s an exciting story you’ve got to read!

Photo

That’s it for this project. There are other things that this app could do:

  • The prompt generator is very basic. It would be neat to have the user generate lists of options.
  • A reminder to the user might be helpful. Perhaps a text message using Twilio (see Project 1) or Tropo (see Project 4), or an email using SendGrid (see Project 5). Maybe even a Yo could work (see Project 2).
  • When the app.js Node.js app restarts, the user must go through the Evernote authorization again. Preserve the user session with connect-mongo.

Source Code

You can find the repo on GitHub.

Post-mortem

This project was really fun. It tackled a problem that finds even the veteran coder’s weak spot, OAuth. However, Evernote’s SDK takes care of it pretty elegantly. Their sample code also showed a way to keep track of user sessions in Node.js. However, these sessions are lost when you restart the Node.js app.

This project also touched on a passion of mine, writing. It’s always fun to just randomly write about things. It is always fun to start a story and have no idea where it’s going to go, just like reading a book. You’re just following a character around and looking into her head to see her passions of balloons and friendships.

15 Projects in 30 Days Challenge

This blog post is part of my 15 projects in 30 days challenge. I’m hacking together 15 projects with different APIs, services, and technologies that I’ve had little to no exposure to. If my code isn’t completely efficient or accurate, please understand it isn’t meant to be complete and bulletproof. When something is left out, I try to mention it. Reach out to me and kindly teach me if I go towards the dark side. ?

This challenge serves a couple of purposes. First, I’ve always enjoyed hacking new things together and using APIs. And I haven’t had the chance (more like a reason) to dive in head first with things like AngularJS, NodeJS, and MongoDB. This project demonstrated AngularJS, Node.js, MongoDB, and Evernote.