Heroku+Nodeで作る!乗り遅れた人のためのFacebookチャットボット開発超入門

2017/02/23

Joyce Echessa

63

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA ASCII Research Laboratories, Inc. Japanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

2016年大ブームになったチャットボット。「そのうち作ってみよう」と思いながらも、なかなか手を動かせていなかった人に贈る、Heroku+Node.jsで始めるFacebookチャットボット開発のススメ。

2016年のF8カンファレンスでFacebookはMessengerプラットフォームを発表しました。Messengerプラットフォームにより開発者は、MessengerもしくはFacebookページで訪問者と会話するボットを開発できるようになりました。ボットを使えば、アプリ開発者はユーザーが大勢でも個々人向けの双方向のやりとりを提供できるので、ユーザーには魅力的でしょう。

サービス開始以降、産業界もアプリ開発者もこのチャットボットに大きな関心を寄せています。発表のわずか3カ月後にはMessengerプラットフォームに推定で約1万1000ものボットが作られました。

チャットボットの恩恵を受けるのは産業界とアプリ開発者だけではありません。ユーザーも、以下のように多くのサービスが利用できるようになります。

チャットボットへの関心の高さは明らかで、今後人工知能(Artificial Intelligence、AI)の技術革新によりボットのコミュニケーションはさらに向上するでしょう。

この記事では、Facebookのページの代わりにMessengerでユーザーと会話ができるチャットボットの作り方を説明します。ユーザーの見たい映画の選択に合わせて異なる会話を提供するボットを作ります。

AIを分かっていないとボットは作れない?

洗練されたボットを作るなら確かにAIに詳しいほうが有利ですが、必須ではありません。機械学習(machine larning)に詳しくなくてもちゃんと作れます。

ボットには2種類あります。1つはルールに基づいたボット、もう1つは機械学習を使うものです。前者はできることに限りがあり、特定のコマンドにしか対応できません。この記事で作るのはこの前者のルールに基づいたボットです。

機械学習を使うボットなら、ユーザーとのコミュニケーションはさらに優れたものになります。コマンドを使うのではなく、人と人との会話のような自然なやり取りができます。さらにボットはユーザーとの会話を通じて学び、どんどん賢くなっていきます。

機械学習を使うボットの作り方は将来の記事のテーマとして取っておきましょう。機械学習を使うボットでさえも、機械学習に関する知識は必要ありません。幸運なことにwit.aiApi.aiのような、開発者が自分のアプリに機械学習(とりわけ自然言語処理)を組み込めるサービスがあります。

さあ始めよう

このデモの完全なコードはここからダウンロードできます。

ボットがFacebookユーザーとコミュニケーションをとるためには、メッセージの送受信や処理をするサーバーの構築が必要です。サーバーはこの処理にFacebook Graph APIを使います。Graph APIがFacebookプラットホームでデータをやりとりする主な手段です。アプリのサーバーはFacebookのサーバーからアクセス可能なエンドポイントURLが必要なので、手元のPCにあるローカルなアプリでは動きません。アプリはオンラインである必要があります。

またGraph APIのバージョン2.5以降、新規サブスクリプションには安全なHTTPSコールバックの使用が義務付けられました。Herokuの初期設定のappname.herokuapp.comドメインはすでにSSLが有効なので、この記事ではアプリをHerokuにデプロイします。Webアプリの作成にはNode.jsを使います。

最初に、自分のマシンにNode.jsがインストールされていることを確認してください。ターミナルからnode -vコマンドで確認できます。インストールされていたらバージョン番号が表示されます。次に、Herokuコマンドラインインターフェイス (CLI)をインストールします。あとでHerokuにアプリをプッシュするのに使います。heroku --versionコマンドでCLIがインストールできたことを確認してください。

プロジェクトフォルダーを作りpackage.jsonファイルを以下のコマンドで初期化します。

$ mkdir spbot
$ cd spbot
$ npm init

表示される指示にしたがって、プロジェクトを設定してください。

package.jsonが初期化されたら、scriptsオブジェクトのところにstartプロパティを追加します。Herokuに対して、アプリを開始するために使うコマンドを通知しています。記事ではアプリのエントリーポイントとしてapp.jsを定義したので、startの値にはnode app.jsを使います。値などはプロジェクトの設定に応じて変更してください。

{
  "name": "spbot",
  "version": "1.0.0",
  "description": "SPBot Server",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node app.js"
  },
  "author": "Joyce Echessa",
  "license": "ISC"
}

以下のようにNodeパッケージをインストールします。

$ npm install express request body-parser mongoose --save

プロジェクトのルートディレクトリに.gitignoreファイルを作成したら、Gitにコミットされないようにするためにnode_modulesフォルダーを含めてください。

node_modules

プロジェクトのルートディレクトリにapp.js (初期値のようにするのならindex.js)というファイルを作成し、以下のように変更します。

var express = require("express");
var request = require("request");
var bodyParser = require("body-parser");

var app = express();
app.use(bodyParser.urlencoded({extended: false}));
app.use(bodyParser.json());
app.listen((process.env.PORT || 5000));

// Server index page
app.get("/", function (req, res) {
  res.send("Deployed!");
});

// Facebook Webhook
// Used for verification
app.get("/webhook", function (req, res) {
  if (req.query["hub.verify_token"] === "this_is_my_token") {
    console.log("Verified webhook");
    res.status(200).send(req.query["hub.challenge"]);
  } else {
    console.error("Verification failed. The tokens do not match.");
    res.sendStatus(403);
  }
});

最初のGETハンドラは、あとでアプリのデプロイに成功したかをテストするためのものです。2つ目のGETハンドラは、Facebookがアプリの認証に使うエンドポイントです。コードは認証リクエストに対してverify_tokenを見てchallengeを返します。

使用するコードには自分のトークン(token)を使ってもかまいませんが、この手のデータは環境変数に保存するのが一番です。この記事でもHerokuにプロジェクト作成したあとで、そのようにします。

Herokuにデプロイする

FacebookプラットホームがバックエンドのWebアプリに接続できるように、アプリをオンラインにしておく必要があります。

Gitレポジトリを作成し、以下のコマンドでプロジェクトファイルをコミットします。

$ git init
$ git add .
$ git commit -m "Initial commit"

ターミナルからHerokuにログインし(アカウントを持っていなければ登録してください)、アプリを作ります。

$ heroku login
$ heroku create
$ git push heroku master
$ heroku open

ここでheroku openコマンドを実行すると、使用しているブラウザーからアプリへのリンクが開くはずです。すべてうまくいったら「Deployed!」と書かれたページが開きます。

環境変数を作成する

先へ進む前に、Herokuの環境変数を作ってアプリの認証トークンを格納します。

Herokuダッシュボードを開き、デプロイしたアプリを選択します。SettingsからReveal Config Var(設定変数を開く)ボタンをクリックしてください。KeyにはVERIFICATION_TOKEN、Valueには自分のトークンを入力し、Addをクリックします。

Create Heroku Config Var

コードの中のトークン文字列「"this_is_my_token"」の部分をprocess.env.VERIFICATION_TOKENに変えてください。変更をコミットしたらHerokuにプッシュします。

Facebookページとアプリを作る

サーバーが稼働したので、Facebookアプリとページを作ります。新規に作ってもいいし、すでにあるものでもかまいません。

Facebookページを作るにはFacebookにログインしてFacebookページを作成を開いてください。ページのタイプは、記事ではエンターテインメントを選択します。

Screenshot of the Create a Page options, showing the six different types of page

次にカテゴリーとページ名を選びます。

Screenshot of the dropdown menu prompting for a category and page name

スタートをクリックするとページが作成され、ページの詳細(説明、Webサイト、プロフィール写真、どのようなユーザー向けかなど)の設定になります。さしあたってこのステップを飛ばして次に進んでもかまいません。

screenshot of the newly created Facebook page

Facebookアプリを作るにはAdd a New App(新しいアプリを作成)を開き、いろいろなプラットホームの選択肢の下にあるbasic setupを選びます。

Screenshot of the 'Add a New App' page, prompting to select a platform

必要な情報を入力してください。カテゴリーはApps for Pages(ページアプリ)を選びます。

Screenshot of the 'Create a New App ID' form

App ID(アプリID)作成をクリックするとアプリのダッシュボードが開きます。

Screenshot of the App Dashboard

「Product Setup(製品の追加)」をクリックすると右側に表示される画面の、Messengerのところで「Get Started(スタート)」を選びます。下のようなメッセンジャーの設定ページが開きます。

Screenshot of the Facebook Messenger Settings page

Messengerのユーザーからのメッセージやその他のイベントをアプリが受け取るには、アプリの「Webhook統合を有効にする」必要があります。これは次項で説明します。Webhook(以前はReal Time Updatesという名称でした)を使えば狙った部分の変更を監視でき、APIを呼び出さなくてもリアルタイムで更新できます。

Webhookの欄にあるSetup Webhooksを選択します。

「callback URL(コールバックURL)」欄に、更新情報の送り先となるこのアプリのエンドポイントURL、<your-app-url>/webhookを入力します。「Verify Token(トークンを確認)」欄には認証用トークン、つまりバックエンドアプリで設定したprocess.env.VERIFICATION_TOKENの値を入力して、下にあるチェックボックスにはすべてチェックを入れます。これはアプリがフォロー(監視)する項目の指定で、それぞれがなにをするのかはあとで説明します。

Webhook Settings

無事Webhookが有効になったら、Webhookの欄には「Complete(完了)」の表示とともに、フォローするイベントが表示されます。もしエラーがあれば、入力したエンドポイントのURLが正しいか(末尾が/webhookになっていること)、Nodeアプリと同じトークンを入力したかを確認してください。

Webhooks panel showing 'complete' message

「Token Generation(トークン生成)」の欄で、ドロップダウンメニューから自分のページを選択します。認証後にページアクセストークンが生成されます。

Token Generation section showing Page Access Token

Herokuでさらに別の環境変数を作り、そのキーをPAGE_ACCESS_TOKENに設定し、さきほど生成されたトークンをその値として入力します。注意してほしいのは、トークンはFacebookで表示されるページに保存されるわけではありません。ページを訪れたときは毎回ページアクセストークンは空の状態で、ドロップダウンメニューからFacebookページを選択するたびに新しいトークンが生成されます。しかし、以前に生成されたトークンは引き続き使えるので、Webページを閉じる前にトークンをコピーするようにします。

Webhookが特定のページのイベントを受け取るには、Webアプリがそのページをフォローする設定になっていなければなりません。Webhookの設定で、フォローするページを選択します。

Subscribe Webhook to Page Events

ウェルカムページ

ボットと新たに会話を始めるユーザーには、ページ名、ページの説明、プロフィール写真、カバー写真といった内容のウェルカムページが最初に表示されます。この画面は「Greeting Text(あいさつメッセージ)」の設定で変更して、ユーザーに対してボットの紹介するのも良いでしょう。

初期設定では会話を始めるにはユーザー側が最初のメッセージをボットに送信する必要があります。しかしGet Started(スタート)ボタンを有効にすればボット側から最初のメッセージを送れます。ボタンが押されるとサーバーにイベントが送信されるので、応答処理ができるわけです。

あいさつメッセージを設定するにはページを開いて、Settings(設定)を開きます。

Page Settings

左側にあるMessagingを選択し、右側の「Messenger Greeting(あいさつメッセージ)」の表示をオンにします。好きなメッセージを設定します。

Customize Greeting Text

スタートボタンを有効にするには、次のコードにあるPAGE_ACCESS_TOKENの部分を自分のトークンに置き換えて、ターミナルに以下のコマンドを追加してください。

curl -X POST -H "Content-Type: application/json" -d '{
  "setting_type":"call_to_actions",
  "thread_state":"new_thread",
  "call_to_actions":[
    {
      "payload":"Greeting"
    }
  ]
}' "https://graph.facebook.com/v2.6/me/thread_settings?access_token=PAGE_ACCESS_TOKEN"

上のコマンドはFacebook Graph APIにリクエストを送信します。成功したら、新規で会話を開始する際のウェルカムページ上にスタートボタンが表示されます。ボタンがユーザーにクリックされるとポストバックコールバックによって、ボットがポストバック(同じページ宛にサーバーから処理を返す)に応答します。

ポストバックは、「ポストバックボタン(Postback button)」「スタートボタン」、「固定メニュー(Persistent menu)」「構造化メッセージ(Structured Message)」タイプのコンポーネントから実行されます。payloadのデータはどのような文字列でもセットできます。アプリ側は文字列を使って、スタートボタンのクリックによるポストバックだと特定します。ポストバックメッセージを受け取るには、アプリはWebhookでフォローしておく必要があります。先ほどWebhookの設定でmessaging_postbacksのところにチェックを入れたのはこのためです。

スタートボタンの設定が正しくできたら、以下のようなレスポンスになります。

{
  "result": "Successfully added new_thread's CTAs"
}

ウェルカムページとスタートボタンは、新規に会話を始めるときにしか表示されないことに注意してください。コードを書いてテストをする際は、これまでの会話を削除して新規に会話を開始します。

ポストバックメッセージを処理するには、Nodeアプリで以下のコードを使ってください。

// All callbacks for Messenger will be POST-ed here
app.post("/webhook", function (req, res) {
  // Make sure this is a page subscription
  if (req.body.object == "page") {
    // Iterate over each entry
    // There may be multiple entries if batched
    req.body.entry.forEach(function(entry) {
      // Iterate over each messaging event
      entry.messaging.forEach(function(event) {
        if (event.postback) {
          processPostback(event);
        }
      });
    });

    res.sendStatus(200);
  }
});

function processPostback(event) {
  var senderId = event.sender.id;
  var payload = event.postback.payload;

  if (payload === "Greeting") {
    // Get user's first name from the User Profile API
    // and include it in the greeting
    request({
      url: "https://graph.facebook.com/v2.6/" + senderId,
      qs: {
        access_token: process.env.PAGE_ACCESS_TOKEN,
        fields: "first_name"
      },
      method: "GET"
    }, function(error, response, body) {
      var greeting = "";
      if (error) {
        console.log("Error getting user's name: " +  error);
      } else {
        var bodyObj = JSON.parse(body);
        name = bodyObj.first_name;
        greeting = "Hi " + name + ". ";
      }
      var message = greeting + "My name is SP Movie Bot. I can tell you various details regarding movies. What movie would you like to know about?";
      sendMessage(senderId, {text: message});
    });
  }
}

// sends message to user
function sendMessage(recipientId, message) {
  request({
    url: "https://graph.facebook.com/v2.6/me/messages",
    qs: {access_token: process.env.PAGE_ACCESS_TOKEN},
    method: "POST",
    json: {
      recipient: {id: recipientId},
      message: message,
    }
  }, function(error, response, body) {
    if (error) {
      console.log("Error sending message: " + response.error);
    }
  });
}

誰かがボットと会話したりページにメッセージ送信した際は、Webhook統合によって更新が送信されます。メッセージを受け取るにはWebhookでPOSTのコールを監視する必要があります。すべてのコールバックはWebhookに向けたものです。

上のPOSTハンドラでは、アプリに送られたメッセージに対し順次処理をしています。ときには複数の束ねられたメッセージが同時に送信されるので、1つのエントリーに複数のオブジェクトが含まれていることもあります。そのため各メッセージエントリーは上のコードを通して型をチェックしています。以下のように、アプリに送られるコールバックメッセージはいろいろな種類があります。

  • Message Received callback:ボットにメッセージが送られた際に送信される。Webhookの設定でmessagesイベントをフォローしておく必要がある
  • Postback Received callback:ポストバックのトリガーに設定したボタンのクリックで送信される。Webhookの設定でmessaging_postbacksイベントをフォローしておく必要がある
  • Message Delivered callback:ページから送信されたメッセージが配信されたときに実行される。Webhookの設定でmessage_deliveriesイベントをフォローしておく必要がある
  • Authentication callback:Send-to-Messenger(メッセンジャーに送信)プラグインがタップされたときに実行される。Webhookの設定でmessaging_optinsイベントをフォローしておく必要がある
  • Message Read callback:ページから送信したメッセージがユーザーに読まれた際に実行される。Webhookの設定でmessage_readsイベントをフォローしておく必要がある
  • Message Echo callback:ページからメッセージを送信した際に実行される。テキストメッセージあるいは添付ファイル(画像、動画、音声、テンプレート、フォールバックなど)を受け取れる。Webhookの設定でmessage_echoesイベントをフォローしておく必要がある
  • Checkout Update callback(ベータ版):可変量のトランザクションで、購入ボタンが押されると実行される。相手の配送先住所によって価格を変更したりできる。Webhookの設定でmessaging_checkout_updatesイベントをフォローしておく必要がある。ただし現在米国以外の地域では使用できない
  • Payment callback:購入ボタンで表示される購入手続きのダイアログで支払いボタンを押したときに実行さる。Webhookの設定でmessaging_paymentsイベントをフォローしておく必要がある。ただし現在米国以外の地域では使用できない

もしイベントがPostbackならprocessPostback()関数をコールして中身をチェックします。思い出してください。スタートボタンでpayload(データの中身)にセットしたのはGreetingでした。そこで、最初にメッセージイベントはスタートボタンがクリックされたかどうかをチェックします。もしクリックされたのであればUser Profile APIでユーザーの名前を取得し、各個人に合わせたメッセージをユーザーに返します。このAPIでユーザーの姓名、プロフィール写真、地域、その地域の時間、性別などが取得できます。

メッセージはsendMessage()関数でメッセンジャープラットホームにポストされます。WebhookのPOSTハンドラには「200 OK HTTP」とレスポンスされます。

「200 OK HTTP」は一刻も早く返すことが重要です。Facebookは次のメッセージ送信の前にこのレスポンスを待つからです。大量の情報を扱うボットで、このレスポンスが遅れるとFacebookからWebhookへのメッセージ配信が滞ってしまいかねません。

もしWebhookからエラー(2xx番以外のステータス)が返ったり、タイムアウト(20秒以上返答がない場合)して、しかも15分以上継続した場合は、警告が返されます。

もしWebhookが8時間にわたって処理に失敗するとFacebookからWebhookを無効にすることが通知され、アプリのイベントのフォローが解除されます。問題を修正したら再度Webhookを設定してイベントのフォローも有効にしなければなりません。

変更を確定しコミットしたらHerokuにプッシュしてください。

実際に会話をしてボットをテストします。facebook.comのページ、モバイルのFacebookアプリ、メッセンジャーの短縮URL:https://m.me/PAGE_USERNAME(ユーザー名の作成は後述)のどこからでもかまいません。

Facebookとメッセンジャーでページ名を検索すると、自分のページが見えます。

Page search

これまで説明してきたように、自分のページ名がほかのページと重複してしまうことがあります。ユーザーが誤ってほかのページを訪れてしまうかもしれません。それを防ぐには、ページに対して一意のユーザー名を設定しましょう。自分のページのホームの、Moreドロップダウンメニューから「Page Info(ページ情報)」の編集を選択します。

More Dropdown Menu

ここでユーザー名を設定してください。

Set Page Username

これでもし@page_usernameで検索すれば正しいページが表示されます。https://m.me/PAGE_USERNAMEからアクセスしても構いません。

以下のように、ウェルカムページにあいさつメッセージとスタートボタンが表示されました。

The Welcome Screen of your Facebook chat bot

ボタンをタップするとサーバーからのメッセージが表示されます。

Initial Message

この状態では、文を入力しても返事はありません。次の項で作成します。

データベースの設定

ユーザーが映画のタイトルを入力したら、ボットはOpen Movie Database APIにより映画の詳細情報を入手します。このAPIリクエストは最初に一致した結果しか返しませんので、返ってきたタイトルはユーザーが意図したものではないかもしれません。そのため、最初にボットはタイトルが正しいかユーザーに確認し、正しければあらすじ、出演者、評価などの詳細を表示します。そこから別の映画タイトルを入力して情報を表示できます。

このやりとりでは、ボットはユーザーが入力した映画タイトルを記憶しておく必要があります。FacebookはWebhookの一連のセッションを開いたままにはしませんので、前回セッションでオブジェクトが持っていたデータは次回リクエスト時には失われています。そこでデータはデータベース、具体的にはMongoDBに保存します。ここではmLabによるHerokuのアドオンを使います。

mLabによりMongoDBは、Database-as-a-Service(サービスとして利用できるデータベース)として利用できます。Herokuでは無料のサンドボックスmlabプランを使えますが、認証のためにクレジットカード情報の登録が必要です。もしカード情報を登録したくなければ、mLabのサイトでアカウント登録して無料プランのsandbox databaseを作成し、コードでそこへリンクしてください(後述)。

Herokuでこのアドオンを使うには、アプリのダッシュボードからResourcesタブを選択します。mlabを検索して選択し、ポップアップ表示のドロップダウンメニューからSandbox - Freeプランを選んだら、Provisionをクリックします。これで、選択したアドオンの確認が表示されます。

MLab Add On

Herokuの環境変数には、MongoDBのURIがすでに設定されています。

MONGO DB Environment Variable

mLabのサイトからデータベースを設定する方法

もしmLabのサイトからMongoDBを設定するならば、アカウントを作成してCreate new deploymentを開いてください。PlanのところをSingle-nodeに変更したら、Standard Lineの欄でSandboxを選択します。

mLab Create Database

データベース名を設定し、Create new MongoDB deploymentボタンをクリックして完了です。

mLab Create Database

続いて開くページで表示されたテーブルから、作成したデータベースを選択します。表示されるページにはデータベースへのアクセス方法が説明されています。

Usersタブを選択し、Add database userボタンをクリックします。ユーザー名とパスワードを入力してCreateをクリックしてください。以上で、アプリがデータベースにアクセスするための、新しい認証情報が登録できました。

ページ上部のデータベースURIを見つけたらコピーしてください。URIは「mongodb://<dbuser>:<dbpassword>@dsxxxxxx.mlab.com:55087/spbot」のような感じです。
作成したdbuser名とパスワードを入力します。Herokuで環境変数MONGODB_URIを作成し、値として先ほどのデータベースURIを入力します。

モデルクラスの定義

Nodeアプリに戻り、movie.jsという名前のファイルを作って、modelsフォルダーに保存してください。ファイルには以下のコードを書きます。

var mongoose = require("mongoose");
var Schema = mongoose.Schema;

var MovieSchema = new Schema({
  user_id: {type: String},
  title: {type: String},
  plot: {type: String},
  date: {type: String},
  runtime: {type: String},
  director: {type: String},
  cast: {type: String},
  rating: {type: String},
  poster_url: {type: String}
});

module.exports = mongoose.model("Movie", MovieSchema);

上のコードは映画のデータベースモデルを作成しています。メッセンジャープラットホームから得たユーザーIDはuser_id、ほかのフィールドにはOpen Movie Database APIから得た情報が入ります。ユーザーが最後に検索した映画だけを保存するので、1ユーザーにつき1レコードだけになります。

user_idを作らずに、各レコードの_idにユーザーIDの使用もできます。FacebookページのユーザーIDが一意であるため、それも可能です。しかし、ユーザーIDのスコープがページ内だけである点に注意してください。つまりある1つのページ中ではユーザーIDは一意ですが、ほかのページにおいてユーザーは別のIDを持っている可能性があるのです。

もしボットが複数の異なるページに対応するなら(そうです、ボットは複数ページにまたがって対応できます)ユーザーIDには注意してください。ボットが複数ページに対応するならFacebookユーザーIDだけで訪問者を特定するのは適切ではなく、同様にデータベースの_idフィールドにユーザーIDを使うのもIDが一意でないため不適切です。複数ページ間にまたがってユーザーIDを1つに特定できないからです。

すべてを仕上げる

データベースとモデルができたのでチャットボットを仕上げます。コードは分割して説明しましたが、すべてのコードを一度に使えるapp.jsファイルへのリンクはこちらです。

データベースへの接続から始めます。Mongooseはほかのモジュールと一緒にインストールされているはずです。

var mongoose = require("mongoose");

var db = mongoose.connect(process.env.MONGODB_URI);
var Movie = require("./models/movie");

WebhookのPOSTハンドラを以下のように変更します。

// All callbacks for Messenger will be POST-ed here
app.post("/webhook", function (req, res) {
  // Make sure this is a page subscription
  if (req.body.object == "page") {
    // Iterate over each entry
    // There may be multiple entries if batched
    req.body.entry.forEach(function(entry) {
      // Iterate over each messaging event
      entry.messaging.forEach(function(event) {
        if (event.postback) {
          processPostback(event);
        } else if (event.message) {
          processMessage(event);
        }
      });
    });

    res.sendStatus(200);
  }
});

ここでイベントはprocessMessage()関数に渡され、messageの型を見てイベントの型のチェックをします。

function processMessage(event) {
  if (!event.message.is_echo) {
    var message = event.message;
    var senderId = event.sender.id;

    console.log("Received message from senderId: " + senderId);
    console.log("Message is: " + JSON.stringify(message));

    // You may get a text or attachment but not both
    if (message.text) {
      var formattedMsg = message.text.toLowerCase().trim();

      // If we receive a text message, check to see if it matches any special
      // keywords and send back the corresponding movie detail.
      // Otherwise, search for new movie.
      switch (formattedMsg) {
        case "plot":
        case "date":
        case "runtime":
        case "director":
        case "cast":
        case "rating":
          getMovieDetail(senderId, formattedMsg);
          break;

        default:
          findMovie(senderId, formattedMsg);
      }
    } else if (message.attachments) {
      sendMessage(senderId, {text: "Sorry, I don't understand your request."});
    }
  }
}

最初に、メッセージがMessage Echo Callbackから送信されたかどうかをチェックします。このコールバックはページからメッセージが送信されると実行されます。たとえば、最初にユーザーに送信するメッセージ(あいさつメッセージ)はWebhookに送り返されます。自分が送ったメッセージは一切処理しなくて良いのでチェックを加えました。

次に、メッセージがテキストのみか添付ファイル(画像、動画、音声)付きかをチェックします。後者であればユーザーに対してエラーメッセージを表示します。テキストであれば、ユーザーが欲しい映画情報を示すキーワードと一致しているかチェックします。この時点でユーザーは映画に関するクエリ(問い合わせ)を送信しているはずで、データベースに保存されます。getMovieDetail()関数はデータベースへ問い合わせを送り、その結果を返します。

function getMovieDetail(userId, field) {
  Movie.findOne({user_id: userId}, function(err, movie) {
    if(err) {
      sendMessage(userId, {text: "Something went wrong. Try again"});
    } else {
      sendMessage(userId, {text: movie[field]});
    }
  });
}

もしユーザーが入力したクエリが、設定済みのどのキーワードとも一致しなければ、ボットは映画に関するクエリだと想定し入力された値を使ってOpen Movie Database APIをコールするfindMovie()関数へ渡します。

function findMovie(userId, movieTitle) {
  request("http://www.omdbapi.com/?type=movie&amp;t=" + movieTitle, function (error, response, body) {
    if (!error &amp;&amp; response.statusCode === 200) {
      var movieObj = JSON.parse(body);
      if (movieObj.Response === "True") {
        var query = {user_id: userId};
        var update = {
          user_id: userId,
          title: movieObj.Title,
          plot: movieObj.Plot,
          date: movieObj.Released,
          runtime: movieObj.Runtime,
          director: movieObj.Director,
          cast: movieObj.Actors,
          rating: movieObj.imdbRating,
          poster_url:movieObj.Poster
        };
        var options = {upsert: true};
        Movie.findOneAndUpdate(query, update, options, function(err, mov) {
          if (err) {
            console.log("Database error: " + err);
          } else {
            message = {
              attachment: {
                type: "template",
                payload: {
                  template_type: "generic",
                  elements: [{
                    title: movieObj.Title,
                    subtitle: "Is this the movie you are looking for?",
                    image_url: movieObj.Poster === "N/A" ? "http://placehold.it/350x150" : movieObj.Poster,
                    buttons: [{
                      type: "postback",
                      title: "Yes",
                      payload: "Correct"
                    }, {
                      type: "postback",
                      title: "No",
                      payload: "Incorrect"
                    }]
                  }]
                }
              }
            };
            sendMessage(userId, message);
          }
        });
      } else {
          console.log(movieObj.Error);
          sendMessage(userId, {text: movieObj.Error});
      }
    } else {
      sendMessage(userId, {text: "Something went wrong. Try again."});
    }
  });
}

該当する映画が見つかったら詳細がユーザーIDと一緒に保存されます。すでに同じユーザーIDのレコードがあった場合はデータを更新します。次に、構造化メッセージを生成しユーザーに送ります。

テキスト以外にも、Messengerプラットフォームでは画像、動画、音声、ファイル、構造化メッセージをユーザーへ送信できます。構造化メッセージはいろいろな状況に対応できるテンプレートです。ボタンテンプレートはテキストとボタン、一般テンプレートは画像、タイトル、サブタイトル、ボタンを送信できます。今回のアプリでは一般テンプレートを使用します。

processPostback()関数を以下のように変更します。

function processPostback(event) {
  var senderId = event.sender.id;
  var payload = event.postback.payload;

  if (payload === "Greeting") {
    // Get user's first name from the User Profile API
    // and include it in the greeting
    request({
      url: "https://graph.facebook.com/v2.6/" + senderId,
      qs: {
        access_token: process.env.PAGE_ACCESS_TOKEN,
        fields: "first_name"
      },
      method: "GET"
    }, function(error, response, body) {
      var greeting = "";
      if (error) {
        console.log("Error getting user's name: " +  error);
      } else {
        var bodyObj = JSON.parse(body);
        name = bodyObj.first_name;
        greeting = "Hi " + name + ". ";
      }
      var message = greeting + "My name is SP Movie Bot. I can tell you various details regarding movies. What movie would you like to know about?";
      sendMessage(senderId, {text: message});
    });
  } else if (payload === "Correct") {
    sendMessage(senderId, {text: "Awesome! What would you like to find out? Enter 'plot', 'date', 'runtime', 'director', 'cast' or 'rating' for the various details."});
  } else if (payload === "Incorrect") {
    sendMessage(senderId, {text: "Oops! Sorry about that. Try using the exact title of the movie"});
  }
}

ここでは2つ、CorrectIncorrectのメッセージのチェックを加えました。ユーザーがボットに対し、表示された映画タイトルが正しいかどうかを回答するボタンから送信されるメッセージです。

コードをコミットしたらHerokuに送信し、ボットへメッセージを送ってみましょう。

ボットにクエリを送り、もし該当する映画が見つかれば、ボットは表示した映画が正しいかどうか問い合わせる構造化メッセージを返します。

Example of a Structured Message from the Facebook chat bot

WebサイトやiOSで見た場合、外観は少し異なります。

Example of the Structured Message output on iOS

構造化メッセージではいくつかのタイプのボタンを使用できます。

  • URLボタン:アプリ内ブラウザーでURLを開く
  • ポストバックボタン:ボットになにかアクションを送信したいときに、Webhookにポストバックを送れる
  • コールボタン:電話番号から電話をかける
  • シェアボタン:シェアダイアログを開き友人とメッセージをシェアできる
  • 購入ボタン:購入手続きを開く

このサンプルでは2つのポストバックボタンを使い、それぞれの値をCorrectIncorrectにセットしました。processPostback()関数を見ると2つのpayload(中のデータ)をチェックしているのが分かります。もしNoボタンがタップされると以下のメッセージが表示されます。

Incorrect Payload Sent

ユーザーは別の映画名で問い合わせをしてきます。

Search for Movie

求めている映画が確定したら、ボットからの案内で会話が進むわけです。

Movie Details

キーワードをprocessMessage()関数でチェックしたことを思い出してください。

ユーザーに送信する構造化メッセージは正しい形式でなければ表示されませんので注意してください。サンプルアプリを使ううち、APIから返された映画タイトルのなかには詳細情報の一部が欠けているものがあることに気がつきました。このような場合、情報のないフィールドの値はN/Aとなります。今回の構造化メッセージでは、APIから返ってきたオブジェクトの中から、Title(タイトル)とPoster(ポスター)という2つのフィールドを使用しました。もし映画が見つかったならタイトルがあるはずなので確認は不要ですが、映画のポスターが無く、代替表示URLも無い場合、構造化メッセージが表示されません。そのため常に構造化メッセージの属性には何らかの値が入っているようにしてください。

今回は、ポスターが無かったときのために代替画像へリンクしました。

Movie with no Poster

Movie with Poster

ユーザーの問い合わせに合う映画があればボットが返します。

Movie not Found

Facebookチャットボットに命を吹き込む

アプリをリリースする用意ができたら、承認手続きを踏まなければなりません。アプリが承認される前は、自分と招待したテスターだけがボットを利用できます。レビューチームへのアプリ提出の手続きは記事の範囲を超えますが、なにをすれば良いのかはガイドで解説されています。

最後に

この記事では、ユーザーからのメッセージを受け取って返信するシンプルなFacebookチャットボットを作りました。このボットは完璧にはほど遠いですが……。ユーザーとの会話で応答できるコマンドはごく限られています。これでは人間同士の会話にはなりません。

ボットはユーザーに対して、コマンドではなく人間同士のやりとりのような自然な応答ができるよう改良されていくでしょう。そのためにはボットに自然言語処理(NLP)を組み込むことです。アプリに自分で自然言語処理エンジンを実装するか、wit.aiのようにアプリに自然言語処理を組み込んでくれるサービスを利用します。wit.aiはFacebookに買収され、個人でも商用でも無料で使えます。

※本記事はJoan Yin、Camilo Reyesが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Building a Facebook Chat Bot with Node and Heroku

[翻訳:西尾健史/編集:Livit

Copyright © 2017, Joyce Echessa All Rights Reserved.

Joyce Echessa

Joyce Echessa

Web開発者で、ときどきモバイル開発も手がけていいます。Twitterの@joyceechessaに記事をアップしています。

Loading...