Node.jsでEvernoteのリマインダー通知機能を実装する


趣味で作っているLunchTimerというアプリを公開してそろそろ1ヶ月くらい経つと思いますが、このアプリは今のところろそこそこ好評で自分のチームでは毎日みんなで投票したりしてランチを決めています。社内でも少しずつ口コミで広まってきてる感もあります。

さて、実際に運用し始めてみると、急遽必要とされている機能が見えてきました。

それは、うっかり投票のし忘れをなんとかならないかw という点です。

業務に集中してると、気づいたら投票の時間過ぎてたなんてことがしばしばあります。
そんな時は、携帯に通知できたら良いなということでEvernoteのリマインダーの機能を使って実装することにしました。

LunchTimerはFacebookの認証を使っているので、Facebook APIを使うのが素直な方法なのですが、どうもnotification API周りを調べてみてもPCでの通知のみでスマートフォンへ通知する方法が無さそうでした。(むしろ、方法知ってる方いたら教えてくださいw)

で、迷ってたところ、evernoteにリマインダーの機能があることに気付き、nodejs用のsdkもあったので使ってみました。これを使えば各個人のevernoteアカウントでログインさえしてもらえば、個人のiPhone・Androidにインストールされたevernoteアプリによる通知が実現できます!

Evernote API Docs
https://dev.evernote.com/intl/jp/doc/

Node用SDK
https://github.com/evernote/evernote-sdk-js

nodejsでevernoteの機能を実装する例ってあまりWeb上探しても無く、さらには結構github上の公式のドキュメントが古かったりで動かなくて苦労したりしました。

0. 前提準備
前提として、API Developer用のkeyを取得します。
https://dev.evernote.com/intl/jp/
API Keyの取得というボタンを押して指示に従うだけです。

また、開発用のsandbox用evernoteアカウントを取得します。
evernote sandbox
https://sandbox.evernote.com/

1. Node用のevernote SDKをinstall

npm install evernote

その後はsdkを使用するファイル内で以下の様にmoduleを読み込みんで、取得したkeyを用いてclientを初期化します。

自作のevernote用のmodule
modules/evernote.js

var Evernote = require('evernote').Evernote;

var evernote = {

    Evernote: null,
    _client: null,

    init: function(){
        this.Evernote = Evernote;
        this._client = new Evernote.Client({
            consumerKey: "xxxxxxxxx",
            consumerSecret: "xxxxxxxxxx",
            sandbox: true
        });
    }
};

2. OAuth認証

express等でauthenticationのリクエストを受け取ったら以下の様に認証の処理を行います。

modules/evernote.js

    authenticate: function(req, res){
        var c = this._client;
        // callbackはアプリ毎に任意のものを指定してください
        var callbackURL = req.param("callback");
        c.getRequestToken(callbackURL, function(error, oauthToken, oauthTokenSecret, results){
            if (error){
                return res.send("Error getting OAuth request token : " + error, 500);
            }
            // store token and secret in session
            req.session.evernote = {authTokenSecret: oauthTokenSecret};
            res.redirect(c.getAuthorizeUrl(oauthToken));
        });
    }

この処理によりevernoteの認証画面が表示されます。
ユーザーがIDとパスワードを入力した後、callbackで指定したURLに戻ってきます。その時に、sessionにauthTokenSecretをストアしておきましょう。後ほど認証成功時に使います。

まずここでredirectで返すと画面は以下のような認証画面に遷移します。

スクリーンショット 2014-07-24 22.53.46

認証完了後、指定したcallbackでは以下のような処理を行います。

callbackを受け取るexpressルーター
routes/evernote.js

// 自作moduleをincludeする
var evernote = require('./modules/evernote');

router.get('/', function(req, res) {
    // this is called as a authentication callback
    var oauthToken = req.param("oauth_token");
    var oauthVerifier = req.param("oauth_verifier");
    // get from session
    var oauthTokenSecret = req.session.evernote && req.session.evernote.authTokenSecret;
    if(!oauthToken || !oauthVerifier || !oauthTokenSecret){
        res.redirect("/");
        return;
    }
    var callback = function(error, oauthAccessToken, oauthAccessTokenSecret, results){
        // store accessToken in session
        req.session.evernote.accessToken = oauthAccessToken;
        evernote.getUser(oauthAccessToken, function(err, user){
            if(err){ res.redirect("/"); }
            // user: {id: xx, username: "xxx", ...}
            req.session.evernote.user = user;
            res.redirect("/");    
        });
    };
    evernote.getAccessToken(oauthToken, oauthTokenSecret, oauthVerifier, callback);
});

先ほどsessionに詰めていたauthTokenSecretがあるかをチェックしてアクセストークンを取得し、そのコールバックでさらにユーザーの情報も取得します。

自作のevernoteのモジュールの方では、単にsdkのmethodをラップしているだけです。getUserの方はきちんと認証したユーザーのaccessTokenを渡してあげます。

modules/evernote.js

    newClient: function(oauthAccessToken){
        return new Evernote.Client({token: oauthAccessToken, sandbox: this._client.sandbox});
    },

    getAccessToken: function(oauthToken, oauthTokenSecret, oauthVerifier, callback){
        this._client.getAccessToken(oauthToken, oauthTokenSecret, oauthVerifier, callback);
    },

    getUser: function(accessToken, callback){
        var client = this.newClient(accessToken);
        var userStore = client.getUserStore();
        userStore.getUser(accessToken, callback);
    },

これでセッションに詰めたユーザーの情報等をUI側に返してあげればOK。認証処理は終わり。

3. evernoteのreminderノートを作成する

認証済みなのでアクセストークンは使い回せる状況です。今回の例ではセッションに保存したアクセストークンを使用します。(本当はDBなど永続的にストアすべきだと思います)

reminderされる時のノートに記載する情報のために、ルーター側でランチのグループ名とランチの時間とタイムゾーンをPOSTで受け付けます。

routes/evernote.js

router.post('/reminder', function(req, res) {
    var accessToken = req.session.evernote && req.session.evernote.accessToken;
    if(!accessToken){
        res.redirect("/");
        return;
    }
    // e.g. {name: "Lunch Group1", lunchTime: "13:00", timezone: "Asia/Tokyo"}
    var entry = req.body;
    var callback = function(err, createdNote){
        res.redirect("/");                    
    };
    // create a new note with reminder
    evernote.createReminderNote(accessToken, entry.name, entry.lunchTime, entry.timezone, callback);
});

あとは、sdkを通じてnoteを作成します。

modules/evernote.js


    createReminderNote: function(accessToken, groupName, lunchTime, timezone, callback){
        var note = new Evernote.Note();
        note.title = "LunchTimer Voting Time Reminder";
        note.content = this._buildReminderNoteContent(groupName, lunchTime);
        this.updateReminder(note, lunchTime, timezone);
        var client = this.newClient(accessToken);
        var noteStore = client.getNoteStore();
        noteStore.createNote(note, function(err, createdNote) {
            if(callback){
                callback(err, createdNote);
            }
        });
    },

    updateReminder: function(note, lunchTime, timezone){
        var now = target = (new Date()).getTime();
        var diff = (moment.tz(lunchTime, "HH:mm", timezone)).diff(moment());
        if(diff >= 0){
            target = now + diff - 600000/*10minutes*/;
        }else{
            // set next day's time 24 hours (1000 * 60 * 60 * 24) + diff - 10 minutes
            target = now + (86400000 + diff - 600000/*10 minutes*/);
        }
        var noteAttr = new Evernote.NoteAttributes({
            reminderOrder: now,
            reminderTime: target
        });
        note.attributes = noteAttr;
    },

まず、createReminderNoteでnoteを作ります。reminderは実はnoteのattributeにセットするだけなので、updateReminderNoteの中でreminderOrderとreminderTimeを設定します。

今回は、設置されているランチの時間から10分前に通知したいため、例を13:00とすると、その日の時点の13:00よりも前だったらただ現在との差分を取れば良く、13:00を過ぎていたら次の日の13:00との差分を取ります。moment.jsとmoment-timezone.jsを使っています。

var moment = require('moment');
var momentTz = require('moment-timezone');

それでは最後に肝心のリマインダーノートに表示される中身の実装です。

modules/evernote.js

    _buildReminderNoteContent: function(groupName, lunchTime){
        var html = ['<?xml version="1.0" encoding="UTF-8"?>'];
        html.push('<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note>');
        html.push('<div style="background: #e6e6e6; color: #585957; font-size: 14px; line-height: 1.3;">');
        html.push('<div style="height: 40px;"> </div><div style="max-width: 600px;padding: 25px 0px; background-color: #fff;margin: 0 auto;box-shadow: 0 0px 5px rgba(0, 0, 0, 0.2); text-align: center;"><div style="margin: 0px 25px;padding-bottom: 15px;">');
        html.push('<img src="https://tejitak.com/lunch/img/logo/logo_64.png"></img>')
        html.push('<h1 style="color: #db6900;margin: 0;margin-top: 15px;font-size: 20px;font-weight: normal;">Lunch Timer Reminder</h1>');
        html.push('<p style="font-size: 16px;margin: 6px 0px;line-height: 1.4;">');
        html.push('This reminder is automatically created by LunchTimer for your group');
        html.push(' "<strong>');
        html.push(groupName);
        html.push('</strong>"</p></div><div style="margin: 0px 25px;font-size: 14px;color: #4d4b47;background-color: #e6f4f6;border: 1px solid #c1e8ec;padding: 0px 15px 8px;clear: both;">');
        html.push('<p style="margin: 8px 0;"><span style="font-weight: bold;">');
        html.push('Please vote for one by configured time');
        html.push(' "<strong>');
        html.push(lunchTime);
        html.push('</strong>"</span></p>');
        html.push('<p style="margin: 8px 0;"><a href="https://tejitak.com/lunch/" style="color: #db6900;">');
        html.push('Open Lunch Timer for a vote');
        html.push('</a></p></div></div><div style="height: 40px;"> </div></div>');
        html.push('</en-note>');
        return html.join("");
    },

簡単に言えばHTMLをコンテントとして渡せばOKです。
ごちゃごちゃしてて、かなりわかりづらいですが、evernoteの提供しているサンプルを少しパクってます。

で、実際にreminderを作って確認してみると以下の様になりました。(sandbox環境)

スクリーンショット 2014-07-24 23.18.32

ちゃんとノートが作成されていて、reminderがセットされてるのが確認できました!

こんな感じでまぁまぁ面倒ではありましたが、さらに繰り返し設定等も実装して、無事使えそうなリマインダー機能実装完了できました。

あ、もしかするとこのアプリevernoteのコンテストに出すかもしれないので、趣味で協力してくださる方募集中〜