LINE BOT APIを使ってAWS Lambdaでサーバレスな版画風写真加工BOTを作る(Node.js)

# はじめに
4月7日に[LINE BOT APIを公開](https://business.line.me/ja/products/4/introduction)してから約2ヶ月がたちました。
公開当初はLet's encryptやStartSSLなどの無料SSL証明書が利用できなかったり(4月18日に追加)、ホワイトリストIPアドレスの設定が必須で、Heroku等のPaaS系サービスを使った環境下では工夫が必要であったり(4月28日にオプションに変更)と一部問題もありましたが、現在はどちらも解消されています。
(参考: [LINE Developer News](https://developers.line.me/news?ny=2016))<br>

そんなわけで、以前であれば多少面倒なこともあったLINE BOTの作成が手軽にできるようになったので、2016年6月頭時点ででている情報をまとめるという目的も兼ねて、最近何かと話題なAWS Lambdaを使ったサーバレスな環境で今更ながらLINE BOT(版画風画像加工BOT)を作成してみたいと思います。

QiitaでもすでにLINE BOTについての記事は相当な数でているので、今回はあまり見られなかった画像加工系のBOTを作成しBOT APIでの画像の受信/送信処理について詳しく説明していきます。

ちなみに今回の対象読者は以下を想定しています。

- これからLINE BOTを作成しようと思っているヒト
- LINE BOTを作成してみたいけどサーバをたてるのが面倒なヒト
- LINE BOTで画像を使った何かをしてみたいヒト

## 版画風画像加工BOTのイメージ
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/1d5ecb7d-ab6d-c17b-bd5b-14fac9d6b314.png" width=460px alt="版画風画像加工BOT完成イメージ">


## BOTシステムの構成図
今回はこのようなシステム構成でLINE BOTを構築していきます。
※ AWS LambdaとAmazon S3は利用料金がかかりますが、Lambdaは100万件/月まで無料で利用できます。
  また、S3もAWSアカウントを作成してから1年以内なら5GBのストレージと2,000Putリクエストの無料枠があります。
(詳しいAWS LambdaとAmazon S3の料金は[「AWS Lambda > 料金」](https://aws.amazon.com/jp/lambda/pricing/)と[「Amazon S3 > 料金」](https://aws.amazon.com/jp/s3/pricing/)を参照ください)

<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/03f6d010-0838-ce20-5aaa-3540e0e68adc.png" width=560px alt="版画風画像加工BOT完成イメージ">


## 事前準備
以下のアカウントだけ事前に取得しておいてください。

- LINE BOT Trialアカウントの取得<br>(参考:[【LINE BOT】 アカウント設定の仕方　〜アカウントを取ってみた！〜](http://qiita.com/Yuki_BB3/items/5c18073181b5eea94164))
- AWSアカウントの取得<br>(参考: [AWS アカウント作成の流れ](https://aws.amazon.com/jp/register-flow/))

# LINE BOT API
## LINE BOTの開発に必要なこと
LINE BOT開発者がLINE BOTを作るには最低限以下の作業が必要です。

- [LINE Business Center](https://business.line.me/ja/)の登録
- BOT API Trialアカウントの取得<br>
  ※ 現在LINEアカウントにつき1つのアカウントのみ取得可能<br>
  ※ フレンド数上限50人 (申請することで2000人まで拡張可能)
- SSL(https)による通信が可能なBOT Serverの構築<br>
  ※ 4月18日にLet's encryptとStartSSLの無料SSL証明書を追加<br>
  ※ [許可済み認証局リスト](https://developers.line.me/wp-content/uploads/2016/04/ca_root.txt)
- リクエストを受け取るCallback URLの登録<br>
  ※ SSL通信、POSTでアクセスできること
- LINE BOT APIによるメッセージの送受信処理


## LINE BOTの一般的な流れ
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/2dddfb5a-bf82-207a-200b-610efd089602.png" width=560px alt="LINE BOT APIの仕組み">

1. ユーザがメッセージを送信 または BOTに対するオペレーション(友達登録/ブロック)を実行
2. LINEプラットフォームがCallback URLに登録されたBOT Serverへリクエストを送信(コールバック)
3. BOT Serverで受け取ったリクエストをあれこれ<br>
   ※ 実際にはリクエストの署名認証などが必要ですが今回は省略します<br>
   ※ (参考: [Signature validation](https://developers.line.me/bot-api/getting-started-with-bot-api-trial#signature_validation))
4. LINE BOT APIを使ってユーザにメッセージを送信


## BOT Serverの条件 〜SSL(https)〜
LINEプラットフォームからのBOT Serverへのコールバックはhttpsで通信されます。
(参考: [Receiving messages/operations](https://developers.line.me/bot-api/getting-started-with-bot-api-trial#receiving_messages_operations))
そのためCallback URLに登録するBOT ServerはSSLによる通信ができることが条件となります。

ただ、今回のようにAWS Lambdaを使えば、API Gatewayにより**デフォルトでhttpsのエンドポイントが作成される**ためSSLのための特別な作業も必要ありません。

## ホワイトリストIPアドレスの登録
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/b0d0e125-820b-f434-51f3-940e25f0e4fb.png" alt="LINE BOT APIの仕組み" width=640>

BOT ServerのIPアドレスをホワイトリストIPアドレスとして登録することによって、それ以外のIPアドレスからのアクセスを制限することができます。
LINE BOT APIの公開当初は必須項目であり、このためIPアドレスが固定でないPaaS系サービス等の利用には工夫が必要でした。
ただ現在はオプション項目となっており、**必ずしもIPアドレスを登録する必要はなくなりました。**

## BOT API Trialアカウントで出来ること
現在はBOT APIアカウントはトライアル版でのみ提供されています。そのためトライアル版では以下の機能が利用可能です。

* メッセージの送受信
* リッチメッセージの送信

# 作成手順
## 1. AWS LambdaによるBOT Serverの構築 〜作成から公開(API Gateway)〜
AWS Lambda及びAmazon API GatewayについてはAmazonが公開している以下のドキュメントを参考にしてください。

- [AWS Lambdaについてのドキュメント](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html)
- [Amazon API Gatewayについてのドキュメント](http://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/welcome.html)

ここではAWS Lambdaを使いサーバレスなBOT Serverを構築し、API Gatewayを利用してhttps通信可能なAPIエンドポイントを取得します。

### 1.1 Lambda関数の作成
AWSにサインインし、ホームから"Lambda"を選択。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/186decbc-309d-b7ce-82d7-f7f9349fee6a.png" alt="AWS Console">

右上のリージョンを確認し、任意のリージョンに変更。
"Get Started Now"からLambda関数を作成します。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/690d54b2-e392-4228-e61e-d0aba8b741c5.png" alt="AWS Console" width=640>

BluePrintの選択です。AWS Lambdaでは事前に用意された多数の雛形(BluePrint)をつかってLambda関数の作成を開始することができます。
今回は特に使用しないで先に進みます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/15c0b0b5-e746-e493-6b96-765389bd0c73.png" alt="BluePrintの選択" width=640>

Lambda関数の各種設定をおこないます。
とりあえずランタイムをNode.js 4.3に、メモリとタイムアウト時間を1024MBと1分くらいにします。
関数の名前と説明は適当につけて大丈夫です。
※ 画像加工処理は時間がかかる処理なのでメモリとタイムアウト時間は適宜調整してください。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/8ad31e70-df49-6964-955b-2ab9a484c838.png" alt="Lambda関数の設定" width=640>


**Lambda関数の作成**
コードのアップロード方法を選択し、Lamda関数を作成します。

- Edit code inline...AWS Lambdaのコンソールで提供されるエディタで直接作成
- Upload a ZIP file...ローカルのファイルをZipで圧縮しアップロード
- Upload a file Amazon S3...Amazon S3からファイルをアップロード

Edit code inlineではAWS Lambdaに事前にインストールされたAWS-SDKライブラリ及び一部ライブラリ(ランタイムにより異なる)のみが利用できます。
それ以外のライブラリの使用や、AWS CLIを利用したい場合は``Upload a ZIP file``または``Upload a file Amazon S3``を選択します。
(参考: [Lambda 実行環境と利用できるライブラリ](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/current-supported-versions.html))

ここでは一旦``Edit code inline``を選択し、以下の最低限のコードのみを作成しておきます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/f4879a3a-1dc1-fb03-267a-caa5c221ab85.png" alt="Lambda関数の作成" width=640>

次にHandlerとRoleの設定をします。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/ce7f83ff-e055-1b09-75b6-fec1c2133bfa.png" alt="HandlerとRoleの設定" width=640>

**Handlerの指定**
HandlerはAWS Lambdaによって呼び出される関数です。AWS LambdaはこのHandlerを呼び出してイベントデータ(event)やランタイム情報(context)を受け渡します。
上の例では``exports.lambdaHandler``としてHandlerを指定します。

**Roleの設定**
Lambda関数の実行権限の設定です。権限にはIAMロールを使用します。
(参考: [IAMとは](http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/introduction.html))

まずは最低限の実行権限である``Basic execution role``を選択すると、IAM Managerが開きます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/1c90a4e0-a587-d067-a982-069de21b248d.png" alt="Lambda関数の作成" width=640>

適当な名前をつけて先に進んでいくと、以下の様な画面が表示されLambda関数は正常に作成されます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/98a099b2-5492-ae7e-a702-2c7cb09cf8a7.png" alt="Lambda関数の作成成功" width=640>


### 1.2 Lambda関数の公開
AWS LmabdaはAPI GatewayによりRESTfulなAPIエンドポイントを作成することできます。
また、API Gatewayではデフォルトでhttpsエンドポイントが作成されるためSSL化のための特別な作業は必要ありません。

まずはAWS LambdaのAPI endpointsタブから"Add API endpoint"を選択。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/a8bb485d-e644-7f1e-0aa6-74f6bd69d5b4.png" alt="APIエンドポイントを追加する" width=640>

"API Gateway"を選択しAPIエンドポイントの設定を行います。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/6a0e6ef9-4379-51f5-af76-f7ba0e6e6179.png" alt="Lambda関数の作成成功" width=640>


**Method**
LINEプラットフォームはBOT ServerへPOSTでコールバックするため、MethodをGETからPOSTに変更します。
(参考: [Receiving messages/operations](https://developers.line.me/bot-api/getting-started-with-bot-api-trial#request_specifications))

**Security**
APIのアクセス制御の設定を行います。設定にはOpen, API Key, IAMを指定できますが今回はあまり考えずLINEプラットフォームから呼び出せるようにOpenに変更します。

以上の設定を変更し、"Submit"を選択するとAPIエンドポイントが作成されます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/cd4da769-accc-7157-b4c7-ae9e28e99167.png" alt="APIエンドポイントの作成成功">


## 2. BOT Serverの登録
### 2.1 コールバックURLの登録
以下のサイトから"Channels"を選択します。
[https://developers.line.me/](https://developers.line.me/)
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/e730b05c-c0e8-2f9f-46c5-a8c5fab00909.png" alt="Channelsに移動">

アカウントがBOT API Trialアカウントになっていることを確認し、Basic informationからEditを選択。

Callback URLの項目に、先ほどAPI Gatewayで取得したAPIエンドポイントURLをポート443を指定して設定します。
※ 現在ポートに443を指定しないとエラーになります。
(エンドポイントURLが``http://serverdomain.com/callback``であれば``http://serverdomain.com:443/callback``と指定する)
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/213e1f0d-699a-0a84-b2a8-1e302b25f700.png" alt="Callback URLの登録" width=640>


### 2.2 テスト送信
正しくメッセージリクエストがBOT Serverにコールバックされるかテストしてみます。
当然まだBOTからの返信はありません。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/942b1aad-61f2-0709-3f7b-6df33fffc67e.png" alt="LINE BOTへのテスト送信" width=640>


### 2.3 ログの確認 ~Amazon CloudWatch~
AWS Lambdaでは標準でAmazon CloudWatchと連携しており、ログをCloudWatchに書き込むことができます。
Node.jsでも``console.log()``や``console.error()``を使ってCloudWatchにログを出力できます。
(参考: [ログ作成 (Node.js)](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-prog-model-logging.html))

AWSコンソールホームからCloud Watchを選択します。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/26dc3afb-5848-82f9-352f-9ff122f45f3d.png" alt="CloudWatchを開く">

"ログ"を選択後表示されるLambda関数のロググループを選択。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/5b462b7f-582c-85d9-2cc4-fdefb68fd399.png" alt="ログの選択" width=640>

イベント時刻から最新のログを選択。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/4643c2b8-a32a-4feb-45a3-0d225105764f.png" alt="ログストリームの選択" width=640>

表示されたログの中身を確認します。
eventオブジェクトの中には先ほど送信したメッセージが含まれているプロパティを確認できます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/34b9a895-fa91-5448-3c4d-697fc0866ff0.png" alt="ログの確認">


## 3 LINE BOTの作成
### 3.1 AWS Lambdaの準備 〜Lambdaで標準ライブラリ以外を利用する〜
今回のBOTでは以下のモジュールを利用します。

- https・・・https通信
- aws-adk・・・Amazon S3の操作
- gm・・・画像処理。node-imagemagickより派生しておりパフォーマンスが高い
- async・・・同期/非同期の制御

また、Lambda(Node.js)では標準モジュール以外に以下のモジュールのみが事前にインストールされています。
(参考: [Lambda 実行環境と利用できるライブラリ](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/current-supported-versions.html
))

- aws-sdk (GitHub: [aws/aws-sdk-js](https://github.com/aws/aws-sdk-js))
- node-imagemagick (GitHub: [rsms/node-imagemagick](https://github.com/rsms/node-imagemagick))

今回は上記以外のモジュールを利用したいので、ローカルでデプロイパッケージを作成しAWS Lambdaにアップロードする方式をとります。
(参考: [デプロイパッケージの作成(Node.js)](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/nodejs-create-deployment-pkg.html))

任意のディレクトリを作成し移動

```
mkdir lambda
cd lambda
```

npmコマンドでgm及びasyncモジュールをインストール

```
npm install gm async
```

Lambda関数を含むjsファイル(filename.js)を作成します

```js:
exports.lambdaHandler = function(event, context){
  //省略
};
```

フォルダの中身をzipで圧縮しデプロイパッケージを作成

```
zip -r ./zipname.zip filename.js node_module
```

AWS Lambdaでcode typeからUpload a .ZIP fileを選択し、先ほどのzipファイルをアップロードします。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/3745a577-f233-146f-b2ec-c96865a66c5f.png" alt="zipファイルのアプロード" width=640>

ConfigurationタブからHandlerを変更します。
もともとのハンドラが``exports.lambdaHandler``の場合は``(filename).lambdaHandler``に変更します。


### 3.2 テキストメッセージの受信
コールバックURLの登録で、すでにLINEプラットフォームからのメッセージの受信は完了しています。
そこで、ここでは受け取ったリクエストbodyからユーザのメッセージ(テキスト)を抽出していきます。

BOT APIリファレンスを参照すると、LINEプラットフォームからコールバックされるリクエストbodyにはresultプロパティが含まれており、そのresultプロパティに配列としてメッセージ情報が格納されていることがわかります。
(参考: [API reference : Getting started with BOT API Trial](Getting started with BOT API Trial))

ただここで注意しなければならないのはresultプロパティには**1つ以上**のメッセージが含まれている可能性があることです。
リファレンスによれば同時に最大100件のメッセージが含まれる可能性があるため、resulatプロパティの配列から１つずつメッセージを取得していく必要があります。
(※ ``result[0]``で最初の要素のみを取り出してはいけない)

```js:
exports.lambdaHandler = function(event, context){
  // Retrieve each message. max:100
  event.result.forEach( function(message, index){
    console.log(JSON.stringify(message));
  });
};
```

各メッセージには以下のプロパティが含まれています。
(参考: [API Reference Property list of messages](https://developers.line.me/bot-api/api-reference#receiving_messages_property))

|プロパティ|説明|
|:------|:-------|
|from|固定値|
|fromChannel|固定値|
|to|BOTのMID|
|toChannel|BOTのチャンネルID|
|eventType|データの種類(メッセージかオペレーション)|
|content|メッセージの中身|

ここで重要なのは``eventType``と``content``です。
``eventType``では受け取ったリクエストがメッセージかオペレーションかを判定することができ、
``content``ではメッセージ及びオペレーションの情報を取得できます。
メッセージの場合の``content``の中身は以下になります。

|プロパティ|説明|
|:------|:------|
|id|メッセージID|
|contentType|メッセージの種類|
|from|ユーザのMID|
|createdTime|リクエストが生成された日時|
|to|メッセージを受け取るユーザのMID(配列)|
|toType|メッセージを受け取るユーザのタイプ|
|contentMetadata|メッセージの詳細|
|text|テキストメッセージ(最大10000字まで)|
|location|位置情報|

``contentType``ではメッセージの種類(テキストか画像か等)を判別することができます。
``from``は、後々ユーザにメッセージを送信する際に必要です。
``text``は、``contentType``の値が1(テキスト)の時にテキストメッセージの内容が格納されます。

すなわちテキストメッセージは基本的に以下の流れで取得できます。

1. resultプロパティから各メッセージを取得
2. 各メッセージのcontent.contentTypeプロパティの値を確認
3. contentTypeが1(テキスト)であれば、content.textプロパティの値を取得


### 3.3 テキストメッセージの送信
LINE BOT APIを使ってユーザにテキストメッセージを送信します。
ここではテキストを送ったユーザに対して「画像を送ってね」と返します。

まずLINE APIを叩くためのヘッダ情報とエンドポイントホストです。
このヘッダ情報とエンドポイントホストは全てのBOT APIコールで共通です。
また、こちらのチャンネルIDとChannel SecretはコールバックURLを登録した際と同様に、LINEのチャンネルコンソール > Basic informationで確認できます。

```js:Header情報
var endpointHost = 'trialbot-api.line.me';  // End Point(Fixed value)
var headers      = {
  'Content-Type':                 'application/json; charset=UTF-8',  //Fixed value
  'X-Line-ChannelID':             'YOUR_CHANNEL_ID',                  //Your channel ID
  'X-Line-ChannelSecret':         'YOUR_CHANNEL_SECRET',              //Your channel secret
  'X-Line-Trusted-User-With-ACL': 'u5b8d5cd3e8f9baf9d7efe0692de87e75' //Fixed value
}
```

ヘッダに含める内容以外では、メッセージの送信には以下が必要です。
(参照: [LINE API Reference | メッセージ送信](https://developers.line.me/bot-api/api-reference#sending_message))

- HTTPメソッド ... POST
- APIエンドポイント ... https://trialbot-api.line.me/v1/events/

またリクエストbodyには以下の内容を含める必要があります。ただし送信先ユーザのMIDと送信テキスト以外は固定値です。

```js:
var body = JSON.stringify({
  to:        [mid],                   //NOTE: 'to' parameter require array of mids
  toChannel: 1383378250,              //Fixed value
  eventType: "138311608800106203",    //Fixed value
  content: {
    contentType: 1,       //Fixed value if send text
    toType:      1,       //Fiexd value if send text
    text:        text
  }
});
```

実際の実装では、メッセージの送信処理をsendTextTo関数に集約し引数としてMIDとtextを渡しています。
これでsendTextTo関数により簡単にメッセージを送れるようになりました。


### 3.3 画像メッセージの受信
画像メッセージの受信フローはテキストとやや異なります。
というのもユーザが画像を送った時に受け取るリクエストには画像データは含まれておらず、
再度画像取得のためのAPIを叩くことではじめて画像データを取得できます。

画像取得のためのAPIは以下が必要です。

- HTTPメソッド ... GET
- APIエンドポイント ... https://trialbot-api.line.me/v1/bot/message/(contentId)/content

こちらも実装ではretrieveImageFrom関数を作成し引数にcontentIdを渡すようにしています。
また、取得は非同期に行われるのでcallback関数も渡します。

**画像データの取得**
http(s)モジュールでは複数回にわけてデータを取得する可能性があるため、取得したデータは順次data配列に格納しておき、
全てのデータを取得した段階でそれら結合する必要があります。

```js:
var data = [];
res.on('data', function(chunk){
  //image data dividing it in to multiple request
  data.push(new Buffer(chunk));
}).on('error', function(err){
  console.log(err);
}).on('end', function(){
  img = Buffer.concat(data);
  callback(null, img);
});
```

### 3.4 サムネイルの作成と保存 ~Amazon S3との連携~
LINE APIで画像メッセージをユーザに送信するためには最低限以下のものが必要になります。
(参照: [LINE API Reference | 画像の送信](https://developers.line.me/bot-api/api-reference#sending_message_image))

- 画像のオリジナルURL ※ 1024×1024以下
- 画像のサムネイルURL ※ 240×240以下
- 送信するユーザのMID

サムネイルをgmのリサイズ処理で作成、画像(オリジナル及びサムネイル)をS3に保存し画像のURLを得ます。
また、ここでは速度向上のためasyncモジュールによりオリジナル画像の保存及びサムネイルの作成・保存は非同期に実行させます。

**GraphicsMagick(gm)を使った画像の加工 〜リサイズ処理〜**
GraphicsMagickについては以下を参考にしてください。

- [GraphicsMagick for node.js](http://aheckmann.github.io/gm/)
- [Graphicsmagick](http://www.graphicsmagick.org/index.html)

画像加工はメソッドチェーンにより複数の画像処理を組み合わせることができます。
また、加工後のデータをイメージバッファーとして取得する場合はメソッドチェーンの最後にtoBufferメソッドを呼び出します。

```js:
gm(img).resize(240).toBuffer('jpg',function(err,buf){
  if(err){
  } else {
  }
});
```

**Amazon S3への画像の保存**
保存のためのS3バケットを作成します。
AWS コンソールホームからS3を選択します。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/c32a8f39-9ccb-f8ac-c2d3-36148b575b96.png" alt="S3の選択">

"バケットを作成"を選択。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/f820a091-7e37-109c-c34e-5bcb908fb897.png" alt="Bucketを作成" width=640>

バケット名とリージョンを選択して作成します。
※ バケット名は一意な名前である必要があります。
(参照: [S3 ドキュメント](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/UG/CreatingaBucket.html))
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/09a859f2-001f-d731-e6ea-c0dc7df04a24.png" alt="Bucketの設定" width=640>

これでAmazon S3の準備は完了です。

S3をLambdaから利用するにはAWS-SDKモジュールを利用します。
**LambdaではAWS-SDKが標準でインストールされており、アクセス権限もデフォルトでついてきます。**そのためIAMロールを適切に与えるだけで利用することが可能です。
(参考: [AWS リソースの管理に関するポリシーの例](http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access_policies_examples.html#iam-policy-example-s3)))
(参考: [Amazon S3 のアクションと条件コンテキストキー](http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/list_s3.html))

Lambda関数を作成するときに作ったIAMロールのポリシーを変更します。

AWSコンソールホームからIAMを選択。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/61101a78-6ec4-c93a-b6d8-8a8b271ecf7b.png" alt="IAMの選択">

"ロール"を選択すると先ほど作成したロールがあるので選択します。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/196bdcce-6c8c-5510-2376-9b5996522048.png" alt="Roleの選択" width=640>

"アクセス許可"タブの"インラインポリシー"の項の"ポリシーの編集"を選択。
<img src="https://qiita-image-store.s3.amazonaws.com/0/96413/8acecae0-3212-2173-d147-ba7e6e15c5d2.png" alt="Policyの選択" width=640>

以下のポリシーに変更し、S3へのバケットの保存権限とバケットのアクセスコントロール(ACL)権限を付与して保存します。

```json:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": [
                "arn:aws:s3:::*"
            ]
        }
    ]
}
```

コードからS3にアクセスするにはバケット名とリージョン名、リージョンエンドポイントが必要になります。
また、S3バケットの各オブジェクトにアクセスするURLは``リージョンエンドポイント/バケット名/オブジェクト名``です。
リージョンエンドポイントとリージョン名は以下を参考にしてください。
(参考: [AWS のリージョンとエンドポイント](https://docs.aws.amazon.com/ja_jp/general/latest/gr/rande.html))

```js:
var s3     = new aws.S3({ apiVersion: '2006-03-01',     //Fixed value
                          region:     'ap-northeast-1'  //Your region name
});
var bucket = 'YOUR_BUCKET_NAME';                        //Your bucket name
var s3Url  = 'https://s3-ap-northeast-1.amazonaws.com/' + bucket + '/'
```

実際の実装では、S3に画像を保存するsaveImageToS3関数を作成し、画像データ、ファイル名を引数としてわたしS3バケットに保存しています。
また、こちらも保存が完了したタイミングでメッセージを送信する必要があるのでcallback関数も合わせて引数に渡します。


### 3.5 画像メッセージの送信
画像メッセージの送信は基本的にテキストメッセージの送信と同じです。
(参考: [LINE API Reference | 画像の送信](https://developers.line.me/bot-api/api-reference#sending_message_image))

ヘッダとメッセージには以下を指定をします。

- HTTPメソッド ... POST
- APIエンドポイント ... https://trialbot-api.line.me/v1/events/

S3への画像の保存後に、リクエストbodyに以下の内容を含めてAPIをコールすることで画像を送信できます。

```js:
var body = JSON.stringify({
  to:        [mid],
  toChannel: 1383378250,            //Fixed value
  eventType: "138311608800106203",  //Fiexd value
  content: {
    contentType: 2,     //Fiexed value if you send image
    toType:      2,     //Fiexed value if you send image
    originalContentUrl: originalUrl,
    previewImageUrl:    previewUrl
  }
});
```


### 3.6 版画風への加工処理
こちらの画像処理を画像(オリジナルとプレビュー)をS3へ保存する前におこないます。
なんちゃって版画風への画像処理であればグレースケール化後に少し広めなフィルタ半径(radius)を使ってエッジ処理するだけです。
(もちろん本格的に版画風にしようとなるともっと複雑です....)

あとはリサイズ時と同じでgmオブジェクトを使ってグレースケール化にmodulateメソッドを、エッジ処理にedgeメソッドを利用します。

```js:
gm(img).modulate(100,0).edge(7).toBuffer('jpg',function(err, buf){
    if(err){
    } else {
    }
});
```

### 3.7 完成
完成した画像加工BOTのソースコードです。
これにより最初の完成イメージどおりに画像を送ってくると版画風にしてくれるBOTが完成しました。

```js:
var https = require('https');                              //HTTPS
var async = require('async');                              //Syncronize
var gm    = require('gm').subClass({ imageMagick: true }); //Image processing
var aws   = require('aws-sdk');                            //AWS SDK

/* LINE API */
var endpointHost = 'trialbot-api.line.me';  //End Point(Fixed value)
var headers      = {
  'Content-Type':                 'application/json; charset=UTF-8',  //Fixed value
  'X-Line-ChannelID':             'YOUR_CHANNEL_ID',                  //Your channel ID
  'X-Line-ChannelSecret':         'YOUR_CHANNEL_SECRET',              //Your channel secret
  'X-Line-Trusted-User-With-ACL': 'u5b8d5cd3e8f9baf9d7efe0692de87e75' //Fixed value
};

/* AWS SDK */
var s3     = new aws.S3({ apiVersion: '2006-03-01',     //Fixed value
                          region:     'ap-northeast-1'  //Your region
});
var bucket = 'YOUR_BUCKET_NAME';                        //Your bucket name
var s3Url  = 'https://s3-ap-northeast-1.amazonaws.com/' + bucket + '/';

/* Other */
var extension = '.jpg';         //Image extention


function sendTextTo(mid, text){
  var options = {
    hostname: endpointHost,
    path:     '/v1/events',
    headers:  headers,
    method:   'POST'
  };
  var req = https.request(options, function(res){
    res.on('data', function(chunk){
    }).on('error', function(err){
        console.log(err);
    }).on('end', function(){            //call when no more date in response
        console.log('finish sending text message')
    });
  });
  var body = JSON.stringify({
    to:        [mid],                   //NOTE: to parameter require array of mids
    toChannel: 1383378250,              //Fixed value
    eventType: "138311608800106203",    //Fixed value
    content: {
      contentType: 1,       //Fixed value if send text
      toType:      1,       //Fiexd value if send text
      text:        text
    }
  });

  req.write(body);
  req.end();
}

function retriveImageFrom(contentId, callback){
  var options = {
    hostname: endpointHost,
    path:     '/v1/bot/message/' + contentId + '/content',
    headers:  headers,
    method:   'GET'
  };
  var req = https.request(options, function(res){
    var data = [];
    res.on('data', function(chunk){
      //image data dividing it in to multiple request
      data.push(new Buffer(chunk));
    }).on('error', function(err){
      console.log(err);
    }).on('end', function(){
      console.log('finish to retrive image')
      img = Buffer.concat(data);
      callback(null, img);
    });
  });

  req.end();
}

function saveImageToS3(img, name, callback){
  var params = {
    Bucket: bucket,
    Key:    name,
    ACL:    'public-read',
    Body:   img
  };
  s3.putObject(params, function(err, data){
    if(err){
      callback("e", "");
    } else {
      callback(null, s3Url + name);
    }
  });
}

function sendImageTo(mid, originalUrl, previewUrl){
  var options = {
    hostname: endpointHost,
    path:     '/v1/events',
    headers:  headers,
    method:   'POST'
  };
  var req = https.request(options, function(res){
    res.on('data', function(chunk){
    }).on('error', function(err){
      console.log(err);
    }).on('end', function(){            //call when no more date in response
      console.log('finish sending image');
    });
  });
  var body = JSON.stringify({
    to:        [mid],
    toChannel: 1383378250,            //Fixed value
    eventType: "138311608800106203",  //Fiexd value
    content: {
      contentType: 2,     //Fiexed value if send image
      toType:      2,     //Fiexed value if send image
      originalContentUrl: originalUrl,
      previewImageUrl:    previewUrl
    }
  });

  req.write(body);
  req.end();
}

exports.lambdaHandler = function(event, context){
  // Retrieve each message. max=100
  event.result.forEach( function(message, index){
    console.log(JSON.stringify(message));
    var mid = message.content.from;
    switch(message.content.contentType){
      case 1:   // Text Message
        sendTextTo(mid, '画像を送ってね');
        break;
      case 2:   // Image Message
        sendTextTo(mid, 'ちょっとまってね');
        async.waterfall([
          function(callback){
            retriveImageFrom(message.content.id, callback);
          },
          function(img, callback){
            //convert image
            gm(img).modulate(100,0).edge(7).toBuffer('jpg',function(err, buf){
                if(err){
                  console.log(err);
                }
                callback(null, buf);
            });
          },
          function(img, callback){
            //save converted image and its thumbnail to S3 bucket
            async.parallel({
              original: function(callback){
                saveImageToS3(img, mid + extension, callback);
              },
              preview: function(callback){
                gm(img).resize(240).toBuffer('jpg',function(err,buf){
                  saveImageToS3(buf, mid + '_thumbnail' + extension, callback);
                });
              }
            }, function(err, result){
              if(err){
                console.log(err);
              } else {
                callback(null,
                         result.original,
                         result.preview);
              }
            });
          },
          function(originalUrl, previewUrl, callback){
            //send converted image
            sendImageTo(mid, originalUrl, previewUrl);
          }
        ], function(err, result){
          if(err){
            console.log(err);
          }
        });
        break;
      default:  // Other Messages
        sendTextTo(mid, '画像を送ってね');
        break;
    }
  });
};
```


## 現状の問題点とまとめ
今回AWS Lambda+API Gatewayを使ってサーバレスに版画風に画像を変換するLINE BOTを作成しました。
ただ、現状のLINE BOTでは少なくとも以下の問題点があります。

- 画像処理に時間がかかる(画像を返信するのに約5秒ほどかかります)
- ユーザ毎にファイル名をわけてオリジナルとプレビューの画像を一枚ずつS3に保存しているので複数同時に送られてきた場合に対処できない。
- エラー処理や例外処理をしていない
- ユーザからのオペレーション(登録/ブロック)に対応していない

これら問題点を解消していけばユーザが加工方法を指定して、色々な画像に加工してくれるBOTなどが作ることができそうです。