Heroku さんようやく堪忍してくれはったわ

twitter bot みたいな形で認証可能にはならんのか、と言いつつ facebook DEVELOPERS から Cloud Services なソレを試してみたら Heroku に Node.js なアプリが launch された模様。
動作確認してみたら動きました。ようやくご勘弁頂けましたか、と言いつつソースを clone してみました。

中身

web.js の中身が以下です。

require.paths.unshift(__dirname + '/lib');

var everyauth = require('everyauth');
var express   = require('express');

var FacebookClient = require('facebook-client').FacebookClient;
var facebook = new FacebookClient();

var uuid = require('node-uuid');

// configure facebook authentication
everyauth.facebook
  .appId(process.env.FACEBOOK_APP_ID)
  .appSecret(process.env.FACEBOOK_SECRET)
  .scope('user_likes,user_photos,user_photo_video_tags')
  .entryPath('/')
  .redirectPath('/home')
  .findOrCreateUser(function() {
    return({});
  })

// create an express webserver
var app = express.createServer(
  express.logger(),
  express.static(__dirname + '/public'),
  express.cookieParser(),
  // set this to a secret value to encrypt session cookies
  express.session({ secret: process.env.SESSION_SECRET || 'secret123' }),
  // insert a middleware to set the facebook redirect hostname to http/https dynamically
  function(request, response, next) {
    var method = request.headers['x-forwarded-proto'] || 'http';
    everyauth.facebook.myHostname(method + '://' + request.headers.host);
    next();
  },
  everyauth.middleware(),
  require('facebook').Facebook()
);

// listen to the PORT given to us in the environment
var port = process.env.PORT || 3000;

app.listen(port, function() {
  console.log("Listening on " + port);
});

// create a socket.io backend for sending facebook graph data
// to the browser as we receive it
var io = require('socket.io').listen(app);

// wrap socket.io with basic identification and message queueing
// code is in lib/socket_manager.js
var socket_manager = require('socket_manager').create(io);

// use xhr-polling as the transport for socket.io
io.configure(function () {
  io.set("transports", ["xhr-polling"]);
  io.set("polling duration", 10);
});

// respond to GET /home
app.get('/home', function(request, response) {

  // detect the http method uses so we can replicate it on redirects
  var method = request.headers['x-forwarded-proto'] || 'http';

  // if we have facebook auth credentials
  if (request.session.auth) {

    // initialize facebook-client with the access token to gain access
    // to helper methods for the REST api
    var token = request.session.auth.facebook.accessToken;
    facebook.getSessionByAccessToken(token)(function(session) {

      // generate a uuid for socket association
      var socket_id = uuid();

      // query 4 friends and send them to the socket for this socket id
      session.graphCall('/me/friends&limit=4')(function(result) {
        result.data.forEach(function(friend) {
          socket_manager.send(socket_id, 'friend', friend);
        });
      });

      // query 16 photos and send them to the socket for this socket id
      session.graphCall('/me/photos&limit=16')(function(result) {
        result.data.forEach(function(photo) {
          socket_manager.send(socket_id, 'photo', photo);
        });
      });

      // query 4 likes and send them to the socket for this socket id
      session.graphCall('/me/likes&limit=4')(function(result) {
        result.data.forEach(function(like) {
          socket_manager.send(socket_id, 'like', like);
        });
      });

      // use fql to get a list of my friends that are using this app
      session.restCall('fql.query', {
        query: 'SELECT uid, name, is_app_user, pic_square FROM user WHERE uid in (SELECT uid2 FROM friend WHERE uid1 = me()) AND is_app_user = 1',
        format: 'json'
      })(function(result) {
        result.forEach(function(friend) {
          socket_manager.send(socket_id, 'friend_using_app', friend);
        });
      });

      // get information about the app itself
      session.graphCall('/' + process.env.FACEBOOK_APP_ID)(function(app) {

        // render the home page
        response.render('home.ejs', {
          layout:   false,
          token:    token,
          app:      app,
          user:     request.session.auth.facebook.user,
          home:     method + '://' + request.headers.host + '/',
          redirect: method + '://' + request.headers.host + request.url,
          socket_id: socket_id
        });

      });
    });

  } else {

    // not authenticated, redirect to / for everyauth to begin authentication
    response.redirect('/');

  }
});

connect-auth ではないのですね。あと、app_id とか app secret とかどうやってるんだろ、と思ったら Readme.md にこのあたりのことが書いてありますね。

また、Heroku への deploy について以下で云々とのこと。

$ heroku create --stack cedar
$ git push heroku master
$ heroku config:add FACEBOOK_APP_ID=12345 FACEBOOK_SECRET=abcde

成程。こーゆー形で facebook さんは Heroku に、なんスね。

中身確認

package.json の中を見てみたら色々モジュールを、な模様。

{
  "name":        "facebook-template-node",
  "version":     "0.0.1",
  "description": "Template app for Heroku / Facebook integration, Node.js language",
  "dependencies": {
    "ejs": "0.4.3",
    "everyauth": "0.2.18",
    "express": "2.4.6",
    "facebook-client": "1.3.0",
    "facebook": "0.0.3",
    "node-uuid": "1.2.0",
    "socket.io": "0.8.4"
  }
}

facebook とか facebook-client って何かな。というか everyauth 含めでこのあたりは nvm bundle install して中身を見ておく必要がありそげ。
と、ゆーことで以下。

$ npm install

で、facebook-client を掘削してみます。始点としては以下かなぁ。

    // initialize facebook-client with the access token to gain access
    // to helper methods for the REST api
    var token = request.session.auth.facebook.accessToken;
    facebook.getSessionByAccessToken(token)(function(session) {

      // generate a uuid for socket association
      var socket_id = uuid();

      // query 4 friends and send them to the socket for this socket id
      session.graphCall('/me/friends&limit=4')(function(result) {
        result.data.forEach(function(friend) {
          socket_manager.send(socket_id, 'friend', friend);
        });
      });

あーと、facebook なナニが始点か。

var FacebookClient = require('facebook-client').FacebookClient;
var facebook = new FacebookClient();

まず facebook-client/lib/facebook-client の中を見てみると index.js があるので中を見てみると以下な記述。

/*
 * This file is part of node-facebook-client
 *
 * Copyright (c) 2010 DracoBlue, http://dracoblue.net/
 *
 * Licensed under the terms of MIT License. For the full copyright and license
 * information, please see the LICENSE file in the root folder.
 */

exports.FacebookClient = require("./FacebookClient").FacebookClient;
exports.FacebookSession = require("./FacebookSession").FacebookSession;
exports.FacebookToolkit = require("./FacebookToolkit");

まずは FacebookClient.js なんスかね。オブジェクト作ってますね。以下が呼び出される模様です。

var FacebookClient = function(api_key, api_secret, options) {
    var self = this;

    this.options = options || {};

使われてる getSessionByAccessToken を探してみると以下な記述。

FacebookClient.prototype.getSessionByAccessToken = function(access_token) {
    var self = this;
    return function(cb) {
        var session = new FacebookSession(self, access_token);
        cb(session);
    };
};

手続きオブジェクト戻してますね。呼び出し側な記述を見てみると以下です。

    facebook.getSessionByAccessToken(token)(function(session) {

      // generate a uuid for socket association
      var socket_id = uuid();

facebook.getSessionByAccessToken 手続きは手続きオブジェクトを引数に取る手続きおオブジェクトを戻すので、そこに手続きオブジェクトを渡しております。って何書いてるのかワケワカっぽいカンジ。戻す手続きオブジェクトで何をしてるかというと、FacebookClient なオブジェクトと access_token を渡して FacebookSession なオブジェクトを生成して callback に渡されております。
で、その callback の中で graphCall という手続きが多用されてます。以下が使用例。

      // query 4 friends and send them to the socket for this socket id
      session.graphCall('/me/friends&limit=4')(function(result) {
        result.data.forEach(function(friend) {
          socket_manager.send(socket_id, 'friend', friend);
        });
      });

ここでも手続き呼び出しの戻りに手続きオブジェクトを渡して呼び出す方式が使われてます。λ 好きだよ λ。
何してるのか、というと FacebookSession の graphCall を呼び出してらっしゃいます。定義されてるのは以下だと思われます。

var FacebookSession = function(facebook_client, access_token) {
    var self = this;

    this.facebook_client = facebook_client;
    
    this.has_access_token = false;
    
    if (access_token)
    {
        self.graphCall = function(path, params, method) {
            method = method || 'GET';
            var authed_params = {
                "access_token": access_token
            };
            
            for (var key in params) {
                authed_params[key] = params[key];
            }
            
            return self.facebook_client.graphCall(path, authed_params, method);
        };

多段式。引数を用意して FacebookClient の graphCall を呼んでますね。この手続きは上記で引用した

var FacebookClient = function(api_key, api_secret, options) {
    var self = this;

    this.options = options || {};

な手続きの中で定義されとります。以下。

    this.graphCall = function(path, params, method) {
        /*
         * Default to take HTTP because it's faster.
         */
        var host = self.options.facebook_graph_server_host;
        var port = self.options.facebook_graph_server_port;
        var secure = false;
        var data = null;
        
        if (params.access_token) {
            /*
             * We have to do a secure request, because the access_token is given. This is HTTPS.
             */
            host = self.options.facebook_graph_secure_server_host;
            port = self.options.facebook_graph_secure_server_port;
            secure = true;
        }
        
        if (method == 'POST') {
            data = params;
        } else {
            path = path + '?' + querystring.stringify(params);
        }
        return doRawJsonRequest(host, port, path, secure, method, data);
    };

ちょい長いスね。access_token 入りなら https にするのかな。ここでも諸々の引数を用意してるだけで doRawJsonRequest 手続きによろしくお願いしておられます。この手続きの定義が以下。

function doRawJsonRequest(host, port, path, secure, method, data) {
    return doRequest(host, port, path, secure, method, data, JSON.parse);
};

移譲の嵐。doRequest 手続きは直上で定義されてまして以下です。ここでは http なナニの記述になってますね。

function doRequest(host, port, path, secure, method, data, parser) {
    return function(cb) {
        var protocol = http;
        if(secure) protocol = https;
        var options = {
            host: host,
            port: port,
            path: path,
            method: method || 'GET'
        };
        var request = protocol.request(options, function(response){
            response.setEncoding("utf8");

            var body = [];

            response.on("data", function (chunk) {
                body.push(chunk);
            });

            response.on("end", function () {
                cb(parser(body.join("")));
            });
        });
        if(data != null) {
            request.write(querystring.stringify(data))
        }
        request.end();
    };
};

しかも手続きを戻すのみ。結果 session.graphCall はパスな情報を受け取ってそこにアクセスする手続きオブジェクトを戻しておられる訳です。

      session.graphCall('/me/friends&limit=4')(function(result) {
        result.data.forEach(function(friend) {
          socket_manager.send(socket_id, 'friend', friend);
        });
      });

で、その手続きオブジェクトに callback な手続きオブジェクトを渡してる、ということになるんですね。なんつーか Scheme 的ソレで非常に素晴しいです。callback 呼び出してるのは "end" の時なので response body が result に渡される模様。

            response.on("end", function () {
                cb(parser(body.join("")));
            });

ということで、上記の forEach の中身を云々してあげればレスポンスな json の処理が可能、ということになりますな。

後で

弄くりマワしてみる方向です。でもこれって Ruby とか選択したら同様に Rails な雛形が Heroku にできちゃったりするのだろうか。

てか

早くこれに気づいてれば connet-auth 云々で時間使うことも無かったし Heroku から ban されることもなかったはずなんですがorz
あと git log 確認したのですが heroku 方面にも Node.js な人が居らっしゃるのですね。ま、当たり前っちゃ当たり前なのか。