mitolab's diary

東南アジアで頑張って生きてる人のブログ

aws serverlessでfacebook messenger botを試してみた

前回のポストから時間がだいぶ空いてしまった...反省。やったことと言えば件名にあることくらいです。まだ中途半端ですが忘れてしまうのでとりあえずメモします。

作ろうと思っているもの

今学んでいるBLOF理論や植物生理、微生物、土の性質、etc...農作物の生産に関するQ&A botができれば便利かなぁと思って着手し始めました。

想定する使い方はいたってシンプルで、

  1. 質問をメッセンジャーに音声で質問する(農作業中テキストを打つとかめんどくさくてしないので)
  2. botがその答えを返してくれる

というもの。


ゆくゆくは個別の圃場情報を予めinputしておいて、例えば「A圃場に必要な肥料は?」とか聞くと、「はい、次の施肥をしてください:① バーク堆肥を250kg、アミノ酸肥料を10kg、鉄肥料を2kg、マンガン肥料を1kg を混ぜて施肥してください ② 酵母菌を30g、砂糖を400g、水を5Lを混ぜて施肥してください、③ 1,2完了後、透明マルチを被せて3日間養生してください」とかそういう答えが返ってくるようなものができれば便利だなぁと思ったりします。つまりは圃場メンテナンスにかけるルーチンプロセスを少しでも省略化することができれば便利だなぁと。


利用サービス

作り方

qiita.com

developers.facebook.com

http://docs.serverless.com/v0.5.0/docsdocs.serverless.com

※ wit.aiについては一端スルー(理由は以下)

現状

頭悪くてwit.aiの使い方/メリットがいまいち理解できず...モチベーションが下がり気味です。"音声認識"がついていること以外、単純にスプレッドシートにキーワードとそのキーワードが含まれていた場合の回答を用意しておくのと何が違うのかイマイチ理解出来ていません...。あと自分がバカなんだとおもいますがwit.ai上の単語の意図するところやインタフェースがわかりにくくてイマイチ手が進まないという。

なので、現状はmessengerでテキストを入力するとオウム返しするbotといういたっておもしろみのないサンプルで止まっています。

f:id:mitolab:20160710154847p:plain

作る上での方針

今回利用するツールはどれも新しい技術らしく、公式ドキュメント、qiita記事などの第三者情報、どちらか一方にしか無い情報があるということがネットサーフィンでわかりました。ので、「ベースの手順は公式ドキュメントを優先し、詰まったところを第三者情報に頼る」という至ってまっとうな形で進めました。

作る上でのメモ

以下は、今後同様のことをするときのために、わかりにくかった点をメモしておきます。

iamロールの設定が必要ですよ

serverlessを導入するにあたって、公式ドキュメントにあるように、"Configuring AWS"から設定しておきましょう。serverlessのプロジェクトを作る前に設定しておかないと、_meta/variables/s-variables-dev-{region名}.jsoniamRoleArnLambdaに適切なキーがアサインされず、handler.jsの実行時にエラーになりますので要注意です。

aws console iamでこんな感じになっていればOK

f:id:mitolab:20160710152220p:plain

iamロールについては、以下が分かりやすかったです。

dev.classmethod.jp

Serverless Optimizer Pluginを入れないと無理

lambdaにUPするコード(ここではhandler.js)にrequireがある場合は、browserifyしてrequire先のコードも1つのファイルにまとめるプラグインが無いと無理でした。

github.com

※ これは前述のqiita記事を読まないとわからなかったです。

API Gatewayからserverlessへのパラメータの受け渡しはどうする?

本来は、s-templates.jsonなるものを作って、任意のendpointに適用していく形のようですが、まどろっこしかったので、qiitaのサンプルのやり方を踏襲してs-function.json内で書けば動いたのでそれでいきました。

{
  "name": "facebook",
  "runtime": "nodejs4.3",
  "description": "Serverless Lambda function for project: FBBot",
  "customName": false,
  "customRole": false,
  "handler": "handler.handler",
  "timeout": 6,
  "memorySize": 1024,
  "authorizer": {},
  "custom": {
    "excludePatterns": [],
    "optimize": {
      "exclude": ["aws-sdk"],
      "transforms": [
        {
          "name": "babelify",
          "opts": {
            "presets": ["es2015"]
          }
        }
      ]
    } 
  },
  "endpoints": [
    {
      "path": "facebook",
      "method": "GET",
      "type": "AWS",
      "authorizationType": "none",
      "authorizerFunction": false,
      "apiKeyRequired": false,
      "requestParameters": {
        "integration.request.querystring.hub.verify_token": "method.request.querystring.hub.verify_token",
    "integration.request.querystring.hub.challenge": "method.request.querystring.hub.challenge",
    "integration.request.querystring.hub.mode": "method.request.querystring.hub.mode"
      },
      "requestTemplates": {
        "application/json": {
          "verify_token": "$input.params('hub.verify_token')",
          "challenge": "$input.params('hub.challenge')",
      "mode": "$input.params('hub.mode')",
          "method": "$context.httpMethod"
        }
      },
      "responses": {
        "400": {
          "statusCode": "400"
        },
        "default": {
          "statusCode": "200",
          "responseParameters": {},
          "responseModels": {
            "application/json;charset=UTF-8": "Empty"
          },
          "responseTemplates": {
        "text/plain": "$input.path('$')"    
          }
        }
      }
    },
    {
      "path": "facebook",
      "method": "POST",
      "type": "AWS",
      "authorizationType": "none",
      "authorizerFunction": false,
      "apiKeyRequired": false,
      "requestParameters": {},
      "requestTemplates": {
        "application/json": {
          "entry": "$input.json('entry')",
          "method": "$context.httpMethod"
        }
      },
      "responses": {
        "400": {
          "statusCode": "400"
        },
        "default": {
          "statusCode": "200",
          "responseParameters": {},
          "responseModels": {
            "application/json;charset=UTF-8": "Empty"
          },
          "responseTemplates": {
            "text/plain": "$input.path('$')"
          }
        }
      }
    }
  ],
  "events": [],
  "environment": {
    "SERVERLESS_PROJECT": "${project}",
    "SERVERLESS_STAGE": "${stage}",
    "SERVERLESS_REGION": "${region}"
  },
  "vpc": {
    "securityGroupIds": [],
    "subnetIds": []
  }
}

参考:

http://docs.serverless.com/docs/templates-variablesdocs.serverless.com

docs.aws.amazon.com

handler.jsはこんな感じ

'use strict';

const request = require('request');

const FB_MESSANGER_TOKEN = 'xxxxxx';

module.exports.handler = function(event, context, cb) {
  switch (event.method.toUpperCase()) {
    case 'GET': {
      if (event.mode === 'subscribe' && event.verify_token === FB_MESSANGER_TOKEN) {
        const validRequest = event.verify_token;
        return cb(null, validRequest ? event.challenge : 'Error, wrong validation token');
      }
    }
    case 'POST': {
      event.entry.forEach(function(pageEntry) {
        pageEntry.messaging.forEach(function(messagingEvent) {
      if (messagingEvent.message) {
        receivedMessage(messagingEvent);
      }
        });
      });
      return cb(null, 'OK');
    }
    default: return cb('Error, Invalid Method');
  } 
};

function receivedMessage(event) {
  var senderID = event.sender.id;
  var recipientID = event.recipient.id;
  var timeOfMessage = event.timestamp;
  var message = event.message; 

  console.log("Received message for user %d and page %d at %d with message:",senderID, recipientID, timeOfMessage);
  console.log(JSON.stringify(message));
  
  var messageId = message.mid;
  
  // You may get a text or attachment but not both
  var messageText = message.text;
  var messageAttachments = message.attachments;

  if (messageText) {
    sendTextMessage(senderID, messageText); 
  } else if (messageAttachments) {
    sendTextMessage(senderID, "Message with attachment received");
  }
}

function sendTextMessage(recipientId, messageText) {
  var messageData = {
    recipient: {
      id: recipientId
    },
    message: {
      text: messageText
    }
  };

  callSendAPI(messageData);
}

function callSendAPI(messageData) {
  request({
    uri: 'https://graph.facebook.com/v2.6/me/messages',
    qs: { access_token: FB_MESSANGER_TOKEN },
    method: 'POST',
    json: messageData

  }, function (error, response, body) {
    if (!error && response.statusCode == 200) {
      var recipientId = body.recipient_id;
      var messageId = body.message_id;

      console.log("Successfully sent generic message with id %s to recipient %s", messageId, recipientId);
    } else {
      console.error("Unable to send message.");
      console.error(response);
      console.error(error);
    }
  });  
}

誰でもbotにアクセスできるわけではない

ここまでの手順で、botサービスを一般公開できるわけではありません。一般公開するには、facebookの審査に通る必要があります。

じゃあどうやって審査に出せばいいの?というと恐らく以下のようになるかと思います。ただし実際に試したわけではないのであくまで推測になります。

  1. facebook developerの当該アプリのダッシュボードにて、左側のサイドバーからmessengerを選択し、スクショの1,2のボタンを押して申請します。

f:id:mitolab:20160710152840p:plain

  1. 左側のサイドバーからアプリレビューを選択し、page_messagingの項目でノートを編集ボタンを押します。

f:id:mitolab:20160710153411p:plain

  1. さらにその先で、プラットフォームポリシーに準拠しているかどうかの確認と、使い方のスクリーンキャストの提出を求められるので、それらを入力します。

f:id:mitolab:20160710153741p:plain

  1. 承認されれば、晴れて一般公開となると思われます。

botを誰かに試して欲しい場合はテスターとして追加する

f:id:mitolab:20160710152447p:plain

画像のように、テスターとして役割に追加してあげれば、botを試してもらうことができます。

感想

今回はとりあえず、serverlessを使ってmessengerでテキストを入力するとオウム返しするbotを作りました。感想としては、おもったよりレスポンスが遅いなー(0.5秒くらいな感覚)と思いました。handler.jsの中身の処理がかさむと恐らくもっと重くなると思われます。

wit.aiは音声認識が付いているので使いたいですが、概念を理解するのに人より多く時間を割かないといけなさそうでだるいなーというところです。

ということで、引き続き触っていきます。