Node.jsのoauthモジュールでTwitterのOAuth認証をしてみた

Expressを触り始めて何か作りたくなったので、とりあえず認証だけ作ってみた。
DB使うのが面倒くさかったのでサクッとできそうなTwitterOAuth認証で、ログイン→認証→リダイレクトして管理画面トップへ、みたいに遷移するとこまでやってみた。

ExpressでTwitter認証は、node.js+socket.io+oauth+SessionWebSocketでログイン付きチャットを作るメモ - すぎゃーんメモがとても参考になった。というかほぼそのままw
Expressのバージョンは2.5なので、"/routes"の下にコントローラファイルがあるのがちょっと違うとこですね。

  • routes/index.js
/**
 * Authorize by oAuth.
 */
exports.auth = function(req, res) {
    var oAuthClient = createOAuthClient();
    var oAuthToken = req.query.oauth_token;
    var oAuthVerifier = req.query.oauth_verifier;
    if (oAuthToken && oAuthVerifier && req.session.oauth) {
        oAuthClient.getOAuthAccessToken(oAuthToken, null, oAuthVerifier,
            function(error, oauth_access_token, oauth_access_token_secret, results) {
                if (error) {
                    res.send(error, 500);
                } else {
                    req.session.user = results.screen_name;
                    res.redirect('/admin/');
                }
            }
        );
    } else {
        oAuthClient.getOAuthRequestToken(function(error, oauth_token, oauth_token_secret, results) {
            if (error) {
                res.send(error, 500);
            } else {
                req.session.oauth = {
                    oauth_token: oauth_token,
                    oauth_token_secret: oauth_token_secret,
                    request_token_results: results
                };
                res.redirect('https://api.twitter.com/oauth/authorize?oauth_token=' + oauth_token);
            }
        });
    }
};

var createOAuthClient = function() {
    return new (require('oauth').OAuth)(
        'https://api.twitter.com/oauth/request_token',
        'https://api.twitter.com/oauth/access_token',
        'CONSUMER KEY',
        'CONSUMER SECRET KEY',
        '1.0',
        '{SITE URL}/auth',
        'HMAC-SHA1');
};

/**
 * Get admin index.
 */
exports.admin = {
    index: function(req, res) {
        console.log(JSON.stringify(req.session));
        res.render('admin/index', {
            title: 'Express',
            username: req.session.user
        });
    }
};


コントローラ上手く分けないともう多いw
でもapp.jsにrequire大量に書くのは嫌だなあ。。みんなどうしてるんだろう。

node-oauthモジュールHTTPSで認証できないみたいな記事もチラホラ見かけましたが、自分が使ったバージョン(0.9.5)では特にエラーが出ることなく認証できました。

CommonJSのPromiseに沿ったシンプルなオレオレDeferred書いた

jQueryのDeferredのデザインにも使われていることから、一番メジャーになりそうなCommonJS Promises/Aの提案を実装してみました。


CommonJS Promises/Aのwikiを読む限りでは、マストな仕様は以下の3つです。

  • Promiseは"未完"な状態から始まり、"未完"あるいは"完了"あるいは"失敗"の状態へ遷移すること
  • Promiseは"then"という名前の関数をもったオブジェクトを返すこと
  • "then"メソッドはPromiseを返しチェインできるようにし、またコールバックでエラーが起きた場合は"失敗"の状態へ遷移すること

あとはそれぞれの開発者が便利なAPIを追加してあげてね!ということらしい。
つまり$.Deferred#resolveとかは仕様にあるわけではなくて、独自に追加された便利APIということになります。
やりたければresolveでコールバック実行するんじゃなくて、"未完"の状態からthenでいきなりコールバック即時実行しちゃってもいいわけです。


個人的にはjQueryの$.Deferredの仕様は普段必要なものを満たしていて好き。
具体的には、状態を遷移させてコールバック(エラーバック)を実行するAPI($.Deferred#resolve, $.Deferred#reject)と、複数の非同期処理の結果を同期的に受け取れるAPI($.when)、非同期でコールバックを実行するAPIJSDeferred#next)は仕事でも使ってるので欲しい。

wikiに載ってる実装一覧を見たところ、状態遷移は"resolve/reject"、複数Deferred管理は"when"という名前が一般的ぽいので合わせておく。
機能追加しやすいよう実装はシンプルに、Node.js/ブラウザ両方からでも同じソースで使えるように。


以下、ソースコード
テストとか含めたものはGitHubにあります。
waka / js-promise-simple

/**
 * @fileoverview Simple implementation of CommonJS Promise/A.
 * @author yo_waka
 */

(function(define) {
define([], function() {

    'use strict';

    // Use freeze if exists.
    var freeze = Object.freeze || function() {};

    
    /**
     * Promise/A interface.
     * @interface
     */
    var IPromise = function() {};

    /**
     * @param {*} value
     */
    IPromise.prototype.resolve;

    /**
     * @param {*} error
     */
    IPromise.prototype.reject;

    /**
     * @param {Function} callback
     * @param {Function} errback
     */
    IPromise.prototype.then;


    /**
     * Implemented Promise/A interface.
     *
     * @param {Object=} opt_scope
     * @constructor
     * @implements {IPromise}
     */
    var Deferred = function(opt_scope) {
        this.state_ = Deferred.State.UNRESOLVED;
        this.chain_ = [];
        this.scope_ = opt_scope || null;
    };

    /**
     * @type {Deferred.State}
     * @private
     */
    Deferred.prototype.state_;

    /**
     * @type {!Array.<!Array>}
     * @private
     */
    Deferred.prototype.chain_;

    /**
     * @type {Object}
     * @private
     */
    Deferred.prototype.scope_;

    /**
     * The current Deferred result.
     * @type {*}
     * @private
     */
    Deferred.prototype.result_;

    /**
     * @return {Deferred}
     * @override
     */
    Deferred.prototype.then = function(callback, errback, progback) {
        this.chain_.push([callback || null, errback || null, progback || null]);
        if (this.state_ !== Deferred.State.UNRESOLVED) {
            this.fire_();
        }
        return this;
    };

    /**
     * @override
     */
    Deferred.prototype.resolve = function(value) {
        this.state_ = Deferred.State.RESOLVED;
        this.fire_(value);
    };

    /**
     * @override
     */
    Deferred.prototype.reject = function(error) {
        this.state_ = Deferred.State.REJECTED;
        this.fire_(error);
    };

    /**
     * @return {boolean}
     */
    Deferred.prototype.isResolved = function() {
        return this.state_ === Deferred.State.RESOLVED;
    };

    /**
     * @return {boolean}
     */
    Deferred.prototype.isRejected = function() {
        return this.state_ === Deferred.State.REJECTED;
    };

    /**
     * Create async deferred chain.
     *
     * @param {Function} callback
     * @param {Function} errback
     * @param {number=} opt_interval
     * @return {Deferred}
     */
    Deferred.prototype.next = function(callback, errback, opt_interval) {
        var interval = opt_interval || 10;

        // create async deferred.
        var deferred = new Deferred(this);
        deferred.then(callback, errback);

        // Add in original callback chain
        this.then(
            function(value) {
                setTimeout(function() {
                    deferred.resolve(value);
                }, interval);
            },
            function(error) {
                setTimeout(function() {
                    deferred.reject(error);
                }, interval);
            }
        );

        return deferred;
    };


    /**
     * @param {*} value
     * @private
     */
    Deferred.prototype.fire_ = function(value) {
        var res = this.result_ = (typeof value !== 'undefined') ? value : this.result_;

        while(this.chain_.length) {
            var entry = this.chain_.shift();
            var fn = (this.state_ === Deferred.State.REJECTED) ? entry[1] : entry[0];
            if (fn) {
                try {
                    res = this.result_ = fn.call(this.scope_, res);
                } catch (e) {
                    this.state_ = Deferred.State.REJECTED;
                    res = this.result_ = e;
                }
            }
        }
    };


    /**
     * @enum {string}
     */
    Deferred.State = {
        UNRESOLVED: 'unresolved',
        RESOLVED: 'resolved',
        REJECTED: 'rejected'
    };
    freeze(Deferred.State);


    /**
     * @return {boolean}
     * @static
     */
    var isPromise = function(arg) {
        return (arg && typeof arg.then === 'function');
    };


    /**
     * @param {..*} var_args
     * @return {Deferred}
     * @static
     */
    var when = function(var_args) {
        var deferred = new Deferred();
        var args = [].slice.call(arguments, 0);
        var results = [];

        var callback = function(value) {
            results.push(value);
            if (args.length === results.length) {
                deferred.resolve(results);
            }
        };

        var errback = function(error) {
            deferred.reject(error);
        };

        for (var i = 0, len = args.length; i < len; i++) {
            var arg = args[i];

            if (isPromise(arg)) {
                arg
                .then(callback, errback)
                .resolve();
            } else if (typeof arg === 'function') {
                (new Deferred())
                .then(arg)
                .then(callback, errback)
                .resolve();
            } else {
                (new Deferred())
                .then(function() {
                    return arg;
                })
                .then(callback, errback)
                .resolve();
            }
        };

        return deferred;
    };

    
    return {
        /**
         * @param {*=} opt_scope
         */
        defer: function(opt_scope) {
            return new Deferred(opt_scope);
        },
        isPromise: isPromise,
        when: when
    };

}); // define
})(typeof define !== 'undefined' ?
    // use define for AMD if available
    define :
    // If no define, look for module to export as a CommonJS module.
    // If no define or module, attach to current context.
    typeof module !== 'undefined' ?
    function(deps, factory) { module.exports = factory(); } :
    function(deps, factory) { this['Promise'] = factory(); }
);

Node.js と MongoDB でナイーブベイズによる文書分類をやってみた

タイトルは若干釣りです>< MongoDB は使ってみたかっただけ。

先月から「言語処理のための機械学習入門」を読んでいるので、ナイーブベイズ分類器を書いてみました。

「言語処理のための機械学習入門」はホントに説明が分かりやすい良本だった(数式全然分からなくてそっちの勉強からのスタートになったけど・・)。
あと、Wikipediaも詳しいです。

ナイーブベイズは実装が簡単な割によく働くテキスト分類手法です。
メールを自動でフィルタリングとかよく見かけるブログのこの記事と似ている記事はこちら、みたいなことをやりたいときに使えそうなやつです。
ナイーブベイズの説明はググればいくらでも出てくるのでカット。
事後確率P(cat|doc)を求める方法として、多項ナイーブベイズモデルとベルヌーイモデルの二つがあるようなのですが、ベルヌーイモデルの方はまだ理解できてない><ので、多項ナイーブベイズモデルで分類しました。


ソースは GitHub にアップしてあります。
node-naivebayes-simple at master from waka/node-naivebayes-simple - GitHub


形態素解析には node-mecab-bindingを使わせてもらいました。
ただ、surfaceが上手く取れてなかったのでparseしたものを分解して使っています。
TinySegmenter を使わせてもらおうかとも思ったのですが、品詞の種別でフィルタしたかったので今回は MeCab で。

MongoDBのアダプタはコールバック地獄を味わってみたかったので、Mongo DB Native NodeJS Driveを使いました。
コールバック深くなりすぎてうんざりしたので富豪的形態素たちを保存しています。

非同期で呼ばれるコールバック内で決定した結果カテゴリを呼び出し側で受け取るために、分類器クラスはEventEmitterを継承したものにしています。


こんな感じで使う。

var Train = require('naivebayes-simple').Train;
Train.train(doc, category); // 学習させて保存
var Classifier = require('naivebayes-simple').Classifier;
Classifier.on('classified', function(arg) {// 分類終わったら[分類カテゴリ, 元文書]が返却される
    var bestCategory = arg[0];
    var originalDoc = arg[1];
    console.log(originalDoc + ' => category: ' + bestCategory);
});
Classifier.classify(doc);

試しにこのブログの過去エントリとカテゴリを学習させて、このエントリを推定したら"javascript"って出てちょっと感動した。

全然関係ないところで、mongodbモジュールが読み込めなくて困ったのだけど、npm install -g したモジュールは、プロジェクトのルートディレクトリで「npm ln "module_name"」して読み込みパスに追加しないとダメみたい。


条件付き確率とか高校以来勉強したので正直ヒーヒーなりましたが、何回も読んでいるうちに数学記号に少しずつ慣れてきた気がします。そういうものだっけか。
仕事で分類器をイチから自分で作ることは無さそうだけど、中でどういうことやってるのかが分かると楽しいですね。

IE8,9で後方互換モードかどうかをチェックする

GmailってIE後方互換モードで見ると「標準モードで見ていってね!」みたいなメッセージを出してくれて親切ですよね。
あれどういう判定の仕方なんだろうと参考にGmailのJSソース見てみたら、TridentがUserAgentに含まれているかどうかで判定しているみたい。
IE8以降ならUserAgentに"Trident"が含まれるので、それとドキュメントモードの判定でいけるよ、ってことみたいです。

TridentはIEのHTMLレンダリングエンジン。Wikipediaがめちゃ詳しい。

function isIECompatibility() {
    var ua = navigator.userAgent;
    if (ua.indexOf('MSIE') == -1) {
        return false;
    } else if (ua.indexOf('Trident') != -1 && document.documentMode && document.documentMode >= 8) {
        return true;
    } else {
        return false;
    }
};

IE10でもTridentはUserAgentに含まれているようなので、当分はこれで大丈夫だと思われる。

delicious が・・・

デザインリニューアルされてる。。。
del.icio.us


これはどういう意図でこういう個人スペースのデザインにしたんだろう。
自分はdeliciousの一番いいところは、ブックマークに必要なメタ情報を上手く見せつつコンパクトに一覧性保ってるところがとても好きで使っていたので、今回のリニューアルはちょっと受けつけなかった。

何人がブックマークしてます、とか1行取るような情報じゃない。トップページのヘッドラインで分かれば十分だよ。
URLも別に出さなくても元々自分がブックマークしたページなんだから無くていい。
日本語タグは表示されてないし、タグの補完も出来なくなった。
ブックマーク用拡張は軒並み動かなくなっているし、公式のブックマークレットは保存する前に開いた時点ですでに保存されているという不具合が・・

これはひどい。ということではてなブックマークに移行。
ああdelicious、個人的にUIが一番好きなWebサービスだったし参考にしたところもあるので、とても思い入れの強いWebサービスだっただけにショック。
ショックで思わずブログに書いちゃうくらいショック。

Google Closure LibraryのDeferredクラスでJSDeferredのloop相当のことをできるようにする

Google Closure LibraryではMochikitのDeferredがサードパーティライブラリとして提供されています(waitとか削られている機能もあるようですが)。
でもMochikitのDeferredではループ処理を遅延処理化する機能は提供されていないので、できるようにしてみました。
JSDeferredでいうところのloop関数に相当するやつです。

goog.provide('cheesepie.async.Deferred');

goog.require('goog.array');
goog.require('goog.async.Deferred');

/**
  * @param {Function=} opt_canceller
  * @param {Object=} opt_defaultScope
  * @constructor
  * @extends {goog.async.Deferred}
  */
cheesepie.async.Deferred = function(opt_canceller, opt_defaultScope) {
    goog.base(this, opt_canceller, opt_defaultScope);
};
goog.inherits(cheesepie.async.Deferred, goog.async.Deferred);

/**
  * @param {Function} canceller
  */
cheesepie.async.Deferred.prototype.setCanceller = function(canceller) {
    this.canceller_ = canceller;
};

/**
  * Asynchronized loop action.
  *
  * @param {Array} iterable The array or arrayLike object.
  * @param {Function} callback The action.
  * @param {number=} opt_wait The wait time until fire next callback.
  * @param {Object=} opt_defaultScope The default scope to call callbacks with.
  * @return {cheesepie.async.Deferred} deferred The deferred instance that is last of deferred chain.
  */
cheesepie.async.Deferred.prototype.forEach = function(iterable, callback, opt_wait, opt_defaultScope) {
    var newDeferred = null;
    var lastDeferred = null;
    var wait = opt_wait || 10;

    // Build recursion deferred chain.
    var deferredSequece = function(currentDeferred, arr, idx, callback, opt_defaultScope) {
        var nextDeferred = new cheesepie.async.Deferred(null, opt_defaultScope);
        var item = arr[idx];
        nextDeferred.addCallback(goog.bind(callback, opt_defaultScope, item, idx));
            currentDeferred.addCallback(function() {
            setTimeout(function() {
                nextDeferred.callback();           
            }, wait);
        });
        if (arr.length - 1 > idx) {
            deferredSequece(nextDeferred, arr, ++idx, callback, opt_defaultScope);
        } else {
            lastDeferred = nextDeferred;
    };
	    
    newDeferred = new cheesepie.async.Deferred(null, opt_defaultScope);
    if (goog.isArray(iterable) && iterable.length > 0) {
        deferredSequece(newDeferred, goog.array.clone(iterable), 0, callback, opt_defaultScope);
    }

    var timerId = null; 	 	 
    var cb = function() {
        timerId = setTimeout(function() {
            newDeferred.callback();
        }, wait);
    };
    newDeferred.setCanceller(function() {
        try {
            clearTimeout(timerId);
        } catch (ex) {
             // pass
        }
    });
    // Add in original chain
    this.addCallbacks(cb, null, opt_defaultScope);
    
    return lastDeferred;
};


使い方は他のコールバック追加と変わらないです。

var arr = [];
var deferred = new cheesepie.async.Deferred();
deferred.addCallback(function(res) {
    console.log(res); // "start"
    arr.push("foo");
    arr.push("bar");
})
.forEach(arr, function(item, idx) {
    console.log(idx, ": ", item); // 100ミリ秒ごとに実行される
}, 100);
.addCallback(function() {
    console.log("finish"); // ループ終了後実行される
});

// fire
deferred.callback("start");


これをなんで作ったかというと、データ行の数が多くて描画時間が数秒かかってしまう(特にIE)UIがあって、他のUIをその間ブロックしてしまう箇所がありまして。
ページャや「もっと読む」を付ければいいじゃんというのもあるのですが、業務アプリ系だとデータがすべて一覧できないと意味が無い場面もあります。
そこで、データ行の描画を後回しにして他のUIの表示/入力を妨げないようにしたというわけです。
実際にデータ行が最後の行まですべて表示されるまでの時間はブロックしちゃう版と変わらないのですが、他のUIをブロックしないだけで体感速度が劇的に違います。
まさかそんなことやらなきゃならないようなもの作る日が来るとはという感じですが、改めてDeferredパターンの素晴らしさを実感できました。


実装の雰囲気は結構違ってますが、この辺のやり方はJSDeferredのソースを読んで勉強しました。
それにしてもムズい!ソースは短いのに理解するのに半日くらい費やしましたよorz モダンへの道は険しい。
ああいうコードをさらで書けるようになりたいなあ。

Google Closure LibraryのユニットテストをPhantomJSから実行できるようにしてみた

結構前の話になりますが、test.jsという勉強会に参加してきました。

そこでt_wada氏のQunitテストをPhantomJSで実行してるやつを見て、とても刺激を受けたので、普段仕事で使ってるGoogle Closure LibraryのユニットテストをPhantomJSで実行できるTAPを作ってみました。

ソースはGithubに公開しています。
(PhantomJSは開発中のv1.2でないと動かないので注意 → 1.2が正式リリースされました!

Closure LibraryのユニットテストはHTMLに書いて実行する上に必要な情報はだいたいconsoleに出力されているので、PhantomJSとの相性がとてもよろしい感じです。

submoduleにClosure Libraryのリポジトリを指定してあるので、"git submodule update --init"して、依存関係の定義ファイルdeps.jsを作ればすぐにサンプルのテストを実行できると思います。