サーバーレスアーキテクチャ（Google cloud function）でLine botを作る

# はじめに
## やること
- Google custom search engine（CSE）で画像を拾ってきて、LINE APIのpushメッセージでその画像を送る
- Google cloud function（GCF）を使ってサーバーレスアーキテクチャで画像を拾ってくるLine botを動かす

## この記事のスコープ外のこと
- Google cloud platform（GCP）やCSEの基本的な使い方
    - 他にいっぱい記事があるのでGCPのアカウント登録やGCFのチュートリアルはすでに終えている前提として話を進める

# Line Messagesing APIを使う
## アカウント登録からチャンネル設定まで
基本[公式ドキュメント](https://developers.line.me/ja/docs/messaging-api/getting-started/)を読んで進めていけば問題ありません。
最終的に今回使う情報は以下の部分です。

基本情報
![チャンネル設定.png](https://qiita-image-store.s3.amazonaws.com/0/122645/1766866e-3e37-614a-619f-2ee7ac653d50.png)

メッセージ送受信設定
![メッセージ送受信設定.png](https://qiita-image-store.s3.amazonaws.com/0/122645/95aecd05-75e9-21c0-af4c-0ed91113a9e7.png)

その他
![その他.png](https://qiita-image-store.s3.amazonaws.com/0/122645/c0b92f88-ee9b-f444-1c89-d7ed28e1325d.png)

## pushメッセージを送ってみる
CSEで画像を拾ってきてそれを自分に対して送ってみる。
CSEの使い方は[こちらの記事](http://ryutamaki.hatenablog.com/entry/2014/01/18/171640)が参考になる。
今回は自分の好きな声優さんの画像を拾ってくることにする。なぜならそちらの方がモチベーションが上がるから（重要）。

- packageインストール

```shell
$ mkdir test_project
$ cd test_project
$ npm install @line/bot-sdk --save
$ npm install request --save
$ npm install config --save
```

- confファイル作成
    - channelAccessTokenとchannelSecretは、LINE developersのコンソールから取得できるアクセストークンとChannel secretである
    - google APIの方のkeyとcxはAPI keyと検索エンジンIDである

```shell
$ mkdir conf
$ vi conf/default.conf
```
```conf/default.conf
{
  "lineAPI": {
      "channelAccessToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "channelSecret":      "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  },
  "googleAPI": {
      "key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
      "cx":  "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  }
}
```

- 画像をCSEから取得してLINEへメッセージをpushするスクリプト作成
    - 今回は画像を送るが、その他にも普通のテキストはもちろん、動画やスタンプ、位置情報等も送れるので[公式ドキュメント](https://developers.line.me/ja/docs/messaging-api/reference/)のメッセージオブジェクトの項目を参照されたし

```
$ vi search_image.js
```
```search_image.js
'use strict'

const line       = require('@line/bot-sdk');
const config     = require('config');
const httpClient = require('request')

const lineClient = new line.Client(config.lineAPI);

const url        = 'https://www.googleapis.com/customsearch/v1';
const searchWord = '立花理香';

function searchImage(searchWord) {

    return new Promise(function(func){

        var pageNumber  = Math.floor(Math.random() * 10) + 1;
        var imageNumber = Math.floor(Math.random() * 10);

        var queryStrings = {
            q: searchWord,
            key: config.googleAPI.key,
            cx:  config.googleAPI.cx,
            searchType: 'image',
            start: pageNumber
        }

        var params = {
            url: url,
            method: 'GET',
            json: true,
            qs: queryStrings
        }

        httpClient.get(params, function(err, res, body) {
            if(err){
                console.log(err);
            }
            try {
                func(res.body.items[imageNumber].link);
            }catch(e){
                console.log('Error: ' + e)
            }
        });

    });
};

function returnUrl(imageUrl){
    return imageUrl;
};

searchImage(searchWord)
    .then(function(imageUrl){
        var message = {
            "type":               "image",
            "originalContentUrl": imageUrl,
            "previewImageUrl":    imageUrl
        };
        lineClient.pushMessage("<Your Line user ID>", message);
    });
```

これを実行すると……

```shell
$ node search_image.js
```

こんな感じに画像が送られてきました。
![push message.png](https://qiita-image-store.s3.amazonaws.com/0/122645/f84c406d-4c1a-d893-58cd-2533ed7a32f4.png)

# サーバーレスアーキテクチャでbotとして動かす

上記の声優画像検索スクリプトを少し改修してbotとして動かしてみます。

```shell
$ vi index.js
```
```index.js
'use strict';

const line         = require('@line/bot-sdk');
const config       = require('config');
const httpClient   = require('request');

const lineClient   = new line.Client(config.lineAPI);

const googleSearch = 'https://www.googleapis.com/customsearch/v1';

function searchImage(searchWord) {

    return new Promise(function(func){

        var pageNumber  = Math.floor(Math.random() * 10) + 1;
        var imageNumber = Math.floor(Math.random() * 10);

        var queryStrings = {
            q:          searchWord,
            key:        config.googleAPI.key,
            cx:         config.googleAPI.cx,
            searchType: 'image',
            start:      pageNumber
        }

        var params = {
            url: googleSearch,
            method: 'GET',
            json: true,
            qs: queryStrings
        }

        httpClient.get(params, function(err, res, body) {
            if(err){
                console.log(err);
                return err;
            }
            try {
                func(res.body.items[imageNumber].link)
            } catch(e){
                console.log('Error: ' + e)
            }
        });

    });
};

function replyWithImage(event, imageUrl){
    console.log(imageUrl);
    var message = {
        "type":               "image",
        "originalContentUrl": imageUrl,
        "previewImageUrl":    imageUrl
    };
    lineClient.replyMessage(event.replyToken, message);
};

function handleEvent(event) {

    var searchWord = '立花理香';

    searchImage(searchWord)
        .then(function(imageUrl){
            replyWithImage(event, imageUrl);
        });

}

exports.webhook = function webhook(req, res) {
    Promise
        .all(req.body.events.map(handleEvent))
        .then((result) => res.json(result))
        .catch((error) => console.error(error));
}
```

これをGCFにアップロードします。処理に数分かかるかもしれません。

```shell
$ gcloud beta functions deploy webhook --trigger-http --source .
Deploying function (may take a while - up to 2 minutes).../
```

exportsしてgcloudコマンドでdeployした関数がGCF上から呼べるようになります。
完了すると以下のようなメッセージが出力されます。

```
...
availableMemoryMb: 256
entryPoint: webhook
httpsTrigger:
  url: https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/webhook
labels:
  deployment-tool: cli-gcloud
...
```
ここで出てきているurlをLine developersのコンソール画面上からWebhook URLに設定します。
設定が完了したら早速botを呼び出してみましょう。
今回書いたコードではなんでもいいので適当なメッセージを送ると画像を返してくれるはずです。
![reply message.png](https://qiita-image-store.s3.amazonaws.com/0/122645/498e2d42-d1c1-b104-6884-b1c250affde9.png)
やったぜ。

# 課題
- CSEで一度に拾ってこれる画像は100枚が限度、また無料枠は一日100リクエスト（それ以上は有料）
    - 画像を大量収集するのには向いてないかな
    - 調べるとBing APIを使ってる人の方が多いっぽい？
- 拾ってきた画像がLINE APIのメッセージオブジェクトの制限に引っかかってるぽくて失敗する（ステータスコード400が返ってくる）時がある
    - originalContentUrlは最大画像サイズ：1024×1024、最大ファイルサイズ：1MB
    - previewImageUrlは最大画像サイズ：240×240、最大ファイルサイズ：1MB