目次


Bluemix と MEAN スタックを使用して自動投稿 Facebook アプリケーションを作成する: パート 3 サーバーにユーザーの代理を務めさせる

Comments

Facebook ユーザーは、自分がログインしていないときでも、自分の代わりにサーバーから Facebook に投稿させたいと思うことがあるものです。例えば、ビジネス・ページのオーナーが、顧客の購買心をあおるために、特定の製品が限られた数量になった時点で、その旨を通知する投稿を送信するという場合です。あるいは単純に、ユーザーが自分のタイムラインにランダムな間隔でメッセージを投稿したいという場合もあります。

こうしたことを実現するためのサーバーを作成するのは可能ですが、簡単ではありません。3 つのチュートリアルからなるこのシリーズでは、IBM Bluemix をクラウド・プロバイダーとして使用して、サーバーから Facebook に投稿させる方法を紹介します。また、シリーズを通して、MEAN スタックを構成する 4 つすべてのコンポーネントの基本も抑えます。具体的な機能をデモンストレーションするために、ランダムなタイミングでユーザーに代わってジョークを投稿するアプリケーションを作成する方法を紹介します。

  • パート 1 では、Facebook をログイン・ソース兼認証メカニズムとして使用する方法を説明します。
  • パート 2 では、Facebook から取得したユーザー情報を保管するように MongoDB を構成する方法を説明します。
  • パート 3 (このチュートリアル) では、Facebook REST API を使用して、サーバーにユーザーの代理を務めさせる方法を説明します。

アプリケーションを作成するために必要となるもの

ステップ 1. ユーザーとして公開する許可を取得する

ユーザーとして公開できるようにするには、次の 2 つが必要です。

  • ユーザーに代わって公開するための許可
  • ユーザーがログインしていなくても使用できる長期トークン

投稿するたびにユーザーの承認を必要とすることなく投稿を公開するには、publish_actions という許可が必要です。ユーザーが最初にログインする時点で、ユーザーに対してこの許可を要求することもできますが、ユーザーは、なぜこの許可が必要なのかを説明するページを読んでからでないと、許可を与えない可能性が高いので、別個のログイン・ウィンドウで許可を要求するのが賢明な方法となります。

また、すでに持っている許可をユーザーに要求して、ユーザーを煩わせたくありません。そうしないためには、代わりに Facebook に許可を要求した上で、publish_actions が必要になった場合にのみ、この許可を要求します。

このプロセスには、次の 3 つのステップがあります。

  1. 現行ユーザーの Facebook ID を取得します。
  2. その ID を使用して許可のリストを取得します。
  3. 必要に応じて、追加の許可を要求します。

Facebook ユーザー ID を取得する

現行ユーザーの ID を取得するには、FB.getLoginStatus 関数を使用します。この関数によって、ユーザー ID とアクセス・トークンが取得されるので、後で使用するときのために、この両方を fbUserInfo という変数に格納します。facebook.js ファイルに、以下のコードを追加します。

// Variable for Facebook user information
var fbUserInfo = {
		id: null,
		token: null,
		hasPublishAccess: false
};

// Get the user information and then run the next
// function
var getUserInfo = function(next) {
	FB.getLoginStatus(function(response) {
		// If we are not connected, do nothing.
		if (response.status != "connected")
			return;
		
		// Fill up the user information structure
		fbUserInfo.id = response.authResponse.userID;
		fbUserInfo.token = response.authResponse.accessToken; 		
		// The access token is used to send requests to Facebook
		// on the user's behalf

		next();
	});
}

現行の許可リストを取得する

ユーザー ID の許可リストを取得するには、「Graph API」という Facebook のREST API を使用します。この API を使用する方法は、チュートリアルのパート 2 「ユーザー情報をサーバーに保管する」で独自の REST API を使用した方法と同様ですが、URL でアクセス・トークンをエンコードしなければならないという点が異なります。理論上、ブラウザー・アプリケーションは、以下の状況であれば、公開許可を確認することができます。

  • 許可に関する情報がすでに入手されている。
  • 許可に関する情報は入手されていないが、ユーザー ID が入手されている。この場合は、checkPubPermInner を使用して Facebook に許可を問い合わせます。
  • ユーザー ID が入手されていない。この場合は、まず getUserInfo を使用してユーザー ID を取得します。その後、next を引数に指定して checkPubPermInner を呼び出します。

facebook.js ファイルに、checkPubPermInner 関数と checkPubPerm 関数を追加します。

// Check if we have publish permissions, assuming we already
// have the user ID. Run next after you have the answer.
var checkPubPermInner = function(next) {
	var url = "https://graph.facebook.com/" + fbUserInfo.id + "/permissions"

	// To get the permissions, use GET with the Facebook Graph API
	$.ajax({
		
		// The HTTP verb we use
		type: "GET",
		
		// Add the authentication token to the URL
		url: url + "?access_token=" + fbUserInfo.token,
		
		// Function called in case this is successful
		success: function(msg) {
			fbUserInfo.hasPublishAccess = false;
			perms = msg.data;
			
			// Check all the permissions
			for (var i=0; i<perms.length; i++)
				if (perms[i].permission == "publish_actions" &
					perms[i].status == "granted")
					fbUserInfo.hasPublishAccess = true;
			next();
		},
		
		// Function called in case this fails
		error: function(msg) {
			alert("Problem getting the permission list for the user "
				+ JSON.stringify(msg));
			// Do not run the next function in case of failure.
		}
	});	
}

// This function checks for publish permissions without making
// any assumptions.
var checkPubPerm = function(next) {	
	// We already have the information, just run next.
	if (fbUserInfo.hasPublishAccess != null)
		next()
		
	// We need to find out	
	else
		if (fbUserInfo.id != null)   // At least we know the user ID
			checkPubPermInner(next);
		else // We need to check the logon status first
			getUserInfo(function() {
				checkPubPermInner(next);
			});			
}

公開許可を要求する

追加の許可について要求するのは、かなり簡単です。すでに、追加の許可を要求する FB.login 関数があるので、あとは適切な条件の下で適切なパラメーターを指定してこの関数を呼び出すだけです。それには、index.html ファイルを編集して、Hello {{userName}} の行の後に以下のスクリプトを追加します。

<script>
// Check for publish permission
checkPubPerm(function() {  
	
	// When this function is called, we
	// know if we have publish permission
							
	// If we don't, ask for it.
	if (!fbUserInfo.hasPublishAccess)  
		FB.login(function(response) {
			// Check if we actually got permission.
			// This requires resetting the variable so
			// checkPubPerm will check again
			fbUserInfo.hasPublishAccess = null;
			checkPubPerm(function() {
				alert("After logon, do we have permission? "
						+ fbUserInfo.hasPublishAccess)
			});
		},
		{ scope: "publish_actions" });  // Permission to ask for
	else	
		alert("No need to ask, we already have permission.");
		
});
</script>

アプリケーションに許可を与えた後、Facebook 開発者向け App Settings にアクセスし、異なる条件下で継続的にコードの動作を確認する許可を削除します。

ステップ 2. 長期トークンを取得する

長期トークンを取得するには、アプリ・シークレット (App Secret) とユーザーがログイン時に受け取るアクセス・トークンをリクエストに含めて Facebook に送信する必要があります。しかし、アプリ・シークレット (App Secret) を秘密にしておこうとするなら、アプリ・シークレット (App Secret) をブラウザーに送信することはできません。攻撃者は、クライアント・サイド・アプリケーションのソース・コードを読み取ることができるためです。このことから、アクセス・トークンをサーバーに送信し、サーバーから Facebook にリクエストが送信されるようにしなければなりません。

アクセス・トークンをサーバーに送信する

ユーザー情報をサーバーに送信するメカニズムとして、すでに putUserInfo があります。したがって、必要な作業は、アクセス・トークンを受け取る関数 getUserInfo がユーザー情報をサーバーに送信するように変更することだけです。それには、getUserInfo 関数を以下のコードで置き換えます。

// Get the user information and then run the next
// function
var getUserInfo = function(next) {
	FB.getLoginStatus(function(response) {
		// If we are not connected, do nothing.
		if (response.status != "connected")
			return;
		
		// Fill up the user information structure
		fbUserInfo.id = response.authResponse.userID;
		fbUserInfo.token = response.authResponse.accessToken; 		
		// The access token is used to send requests to Facebook
		// on the user's behalf
		
		// Send the user information to the server
		putUserInfo({id: fbUserInfo.id,
			shortToken: fbUserInfo.token
		});

		next();
	});
}

Facebook から長期トークンを取得する

app.js ファイルの中で、app.put 関数を呼び出すことによって短期トークンを受け取った後、サーバーは以下のリクエストを Facebook に送信する必要があります。

https://graph.facebook.com/v2.1/oauth/access_token

このリクエストでは、以下のパラメーターを使用します。

パラメーター
grant_type fb_exchange_token
client_id App ID (アプリ ID)
client_secret App Secret (アプリ・シークレット)
fb_exchange_token ユーザーのアクセス・トークン

app.put 関数の呼び出しを以下のコードで置き換えます。

// We need issue an HTTPS request
var https = require("https");

// PUT is used to update existing entries. If it includes a
// short-term token, replace it for a long-term one and store that.
app.put(restUserPath + "/:id", bodyParser.json(), function(req, res) {
	// If we got a short-term token, use it. Also, remove it from
	// req.body so it won't be stored.
	if (req.body.shortToken != undefined) {		
		// Get the long-term access token directly from Facebook.
		https.get("https://graph.facebook.com" +
				"/v2.1/oauth/access_token?" +
				"grant_type=fb_exchange_token&" +
				// Replace client_id and client_secret with the values
				// for your application
				"client_id=***REPLACE WITH YOUR VALUE***&" + 				
				// Note that client_secret NEVER gets to the browser.
				// We have to assume that anything we send to the
				// browser could be used against us.
				"client_secret=***REPLACE WITH YOUR VALUE***&" +
				"fb_exchange_token=" + req.body.shortToken,
				function(res) {
					// This function is called as soon as we get the
					// HTTP headers. Unfortunately, the access token
					// is provided in the HTTP body, the "HTML" file
			
					  res.on('data', function(body) {
					  // The access token is provided in the form of
					  // an HTTP query, along with one other parameter
						  var token = body.toString().
						  	replace(/&.*$/, ").
						  	replace("access_token=", ");		  
				// Finally, write the access token. Because we're in an
				// inner function called much later,  
				// call userCollection.update independently
						  userCollection.update({"id": req.params.id},
								{$set: {"id": req.params.id,
										"token": token}
								});
						  console.log("id:" + req.params.id);
						  console.log("token:" + token);
					  });
				}
		);		
		req.body.shortToken = undefined;
	}
	
	// In a MongoDB update, you can use the command $set followed by an associative
	// array of all the fields you wish to set and their new values.
	userCollection.update({"id": req.params.id}, {$set: req.body},
				{upsert: true});
	
	res.send();
});

トークンがデータベースに書き込まれることを確認するには、ユーザー・リストを返す app.get 呼び出しに変更を加えます。以下のコードを使用して、データを送信するループを変更します。ただし、これらのトークンは機密情報なので、確認し終わったら、変更したループは必ず元に戻してください。

			// Only send the users' names
			for (var i=0; i<items.length; i++)
				censored[i] = {
					name: items[i].name,
					token: items[i].token  // DELME
				};

ステップ 3. ジョークを投稿する

ユーザーに代わって Facebook に投稿する

  1. Facebook に投稿するのは、とても簡単です。app.js ファイルに以下のコードを追加すれば、適切な POST リクエストを Facebook に送信することができます。
    var querystring = require('querystring');
    
    // Post a joke to this ID and token. To post to a facebook page,
    // use the HTTP POST.
    var postJoke = function(id, token, joke) {
    	
    	// Data, the message to post, and the access token to
    	// show we are permitted to post it.
    	var data = {
    			access_token: token,
    			message: joke
    	};
    	
    	// Encode the data
    	var post_data = querystring.stringify(data);
    	
    	// The options that go in the HTTP header
    	var post_options = {
    	      host: 'graph.facebook.com',
    	      path: "/v2.1/" + id + "/feed",
    	      method: 'POST',
    	      headers: {
    	          'Content-Type': 'application/x-www-form-urlencoded',
    	          'Content-Length': post_data.length
    	      }
    	};
    
    	// Set up the request
    	var post_req = https.request(post_options, function(res) {
    		// If we get any response, log it. It could be a valid error
    		// message.
    	    res.setEncoding('utf8');
    	    res.on('data', function (chunk) {
    	        console.log('Response: ' + chunk);
    	    });
    	});
    
    	// Post the data and close the request
    	post_req.write(post_data);
    	post_req.end();
    };
  2. この関数が正常に動作することを確認するには、app.js ファイルで以下のような呼び出しを設定します。
    postJoke("<your ID goes here>",
    		"<your token goes here>",
    		"Joke goes here");

すべてのユーザーに代わって投稿する

次のステップでは、postJoke とデータベース・クエリーを組み合わせて、投稿を送信する許可を与えてくれたすべてのユーザーにジョークを送信します。そのためのコードは以下のとおりです。

// Send the joke to all the users for which we have a token.
var sendOutJoke = function(joke) {
	
	// If the userCollection is not available yet,
	// wait a second and try again.
	if (userCollection == null) {
		setTimeout(function() {sendOutJoke(joke);}, 1000);
		return ;
	}
	
	// Find all users that have a token in the database. The
	// token might be expired or revoked, but then Facebook
	// will just ignore our request
	userCollection.find({"token": {$exists: true}},
			// {$exists: true} means that we look for any document
			// in which the attribute (token, in this case) exists.
						{}, // No options
		function(err, cursor) {
			if (err) {
				console.log("Search error in getting users to send jokes");
				console.log("Stack:");
				console.log(err.stack);			
			} else
				// For all the users we found
				cursor.forEach(function(user) {
					// Actually send the joke
					postJoke(user.id, user.token, joke);
				});			
		});  // end of userCollection.find
};

ステップ 4. ジョークの投稿をスケジューリングする

最後に、ランダムなタイミングでジョークを投稿するために、app.js ファイルに以下のコードを追加します。

// Wait a random amount of time and then send a joke
var waitJoke = function() {
	setTimeout(function() {
		// Pretend we have a thousand jokes available.
		sendOutJoke("Joke #:" + Math.floor(Math.random()*1000));

		// Repeat the cycle
		waitJoke();
	},
	
	// setTimeout receives a parameter in miliseconds.
	// We want to wait 10-15 hours before
	// sending the next joke.
	1000*3600*(10 + Math.random()*5));
};


// Start sending jokes
waitJoke();

ステップ 5. 共有シークレットにパラメーターを使用する

変更する可能性のある情報は、コードに直接含めるのではなく、環境パラメーターに格納するのが賢明です。そのような情報としては、共有シークレットが挙げられます。何らかの理由で共有シークレットが外部に漏れた可能性が疑われる場合は、(Facebook のアプリケーション開発ページで) 共有シークレットをリセットする必要があります。

  1. app.js ファイルを開きます。アプリケーション・シークレットが使用される場所は、app.put(restUserPath + "/:id"... 呼び出しの中の https.get リクエストだけです。URL の client_secret の部分を指定する行を、以下のコードで置き換えます。
    				"client_secret=" + process.env.FB_APP_SECRET + "&" +
  2. Bluemix ダッシュボードで、アプリケーションのタイルをクリックします。
  3. 左側にある「Environment Variables (環境変数)」をクリックします。
  4. 「USER-DEFINED (ユーザー定義)」をクリックし、次に「ADD (追加)」をクリックします。
  5. 新しい変数に FB_APP_SECRET という名前を付けて、シークレットの値 (Facebook アプリケーション・ダッシュボードで確認できます) を入力します。
  6. 「SAVE (保存)」をクリックします。これにより、正しい値を使用してアプリケーションが再起動されます。

アプリケーションを再デプロイすると、この環境変数が削除されてしまいます。したがって、実際の開発が完了した後で、環境変数を使用する方法に切り替えることをお勧めします。

許可に関する問題

サンプル・アプリケーションのソース・コードをダウンロードして Facebook で手順に従ってみると、このアプリケーションを実行すれば、アプリケーションから自分のタイムラインに投稿させられることがわかるはずです。ただし、他のユーザーがそれをすることはできません。その理由は、Facebook はアプリケーションに (開発者以外の) ユーザーのタイムラインに公開させることについて、非常に慎重だからです。開発者以外のユーザーに publish_actions を要求できるようにするには、アプリケーションが Facebook による手作業での承認に合格しなければなりません (この承認は、Facebook のアプリケーション設定で要求します)。さらに困難なことに、このチュートリアルのシリーズの例では、ユーザーの承認なくして特定のメッセージを公開することは明示的に禁止されます。

まとめ

このサンプル・アプリケーションは、MEAN スタック、Facebook、Bluemix の概要、そしてこの 3 つすべてを統合する方法を説明するために作成されています。説明のために単純化されているので、下記の重要な機能はありません。

  • ユーザーがアプリケーションへの登録を解除する手段 (Facebook で許可を取り消すのとは別のことです)
  • ランダムな番号の「Joke #:」ではなく、MongoDB の実際のジョークのコレクションを送信する機能
  • 長期トークンの有効期限を追跡して、トークンが有効期限に近づくと、許可の再付与を要求する e-メールを送信する機能
  • 何らかの統計で、各ジョークが「Like (いいね!)」ボタンや「Share (シェア)」ボタンをクリックされた回数を確認し、ジョークのコレクションを最適化する機能

以上の不備はあるにせよ、このシリーズの内容が MEAN、Facebook、Bluemix を併せて使用する方法を説明するのに十分であったことを願います。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Cloud computing, Web development
ArticleID=1012710
ArticleTitle=Bluemix と MEAN スタックを使用して自動投稿 Facebook アプリケーションを作成する: パート 3 サーバーにユーザーの代理を務めさせる
publish-date=08202015