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 にこのあたりのことが書いてありますね。
- ローカルで動かす場合
- npm bundle install せよ
- .env に以下な記述を追加せよ
- foreman start
- foreman については http://blog.daviddollar.org/2011/05/06/introducing-foreman.html を参照のこと
また、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 の処理が可能、ということになりますな。
てか
早くこれに気づいてれば connet-auth 云々で時間使うことも無かったし Heroku から ban されることもなかったはずなんですがorz
あと git log 確認したのですが heroku 方面にも Node.js な人が居らっしゃるのですね。ま、当たり前っちゃ当たり前なのか。