【Python3】LINE APIとLambda連携〜最速・速習メソッド〜

## コンセプト
AWS Lambdaを使用したLINEmessagingAPI使用方法を、
外部連携に重点を置いて解説します。
ソースは極力関数化をし、図やサンプルコード・リンクをなるべく多く使うようにしているので、
初心者にもとっかかりやすい内容になっていると思います。

## 全体像
LINEからAPI Gatewayをエンドポイントとし、SlackやGoogle Apps Script(GAS)に連携する。
GASにはGoogle Driveへの画像アップロード、Slackにはテキスト通知を担当してもらいます。


![Untitled Diagram-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378268/0354fc98-bdf7-3586-256e-49a69a7c4549.jpeg)

## LINEアカウント作成
まず、LINE Developersアカウントを作成してください。
[作成手順はこちら](https://jp.fujitsu.com/solutions/cloud/k5/playground/webhandson/preparation/06.html)

**※ここで取得した以下の内容をメモに残しておく**
・アクセストークン
・Channel Secret

## LambdaとAPI Gatewayの作成
API Gatewayについて、今回は特段設定する必要はありません。
[詳しい解説はこちら](https://qiita.com/UpAllNight/items/8a347be69db7653657d0)
以下の手順に従って、順々に作成・設定してください。

1.コンソール画面からLambdaの画面へ
![スクリーンショット 2019-06-14 20.28.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378268/cd0202d5-0544-4042-081d-299e1d6553d1.png)

2.関数の作成画面で関数名を入力し、「関数の作成」をクリック
※ランタイムはPythonの最新版を選択
![スクリーンショット 2019-06-14 20.31.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378268/05583b90-a65a-23a5-6763-1f9395e0db81.png)

3.トリガーの追加から「API Gateway」を選択
![スクリーンショット 2019-06-14 20.34.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378268/cc880d1e-f222-3e8e-db29-b1216030ddc5.png)

4.トリガーの設定で「新規APIの作成」を選択し、セキュリティを「オープン」に設定
![スクリーンショット 2019-06-14 20.37.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378268/1bc22631-4527-5ed7-abe8-188703a118ed.png)

5.右上の「保存」をクリック

**※ここで取得した以下の内容をメモに残しておく**
・API Gatewayの「API エンドポイント」

## LINEアカウントの設定
再び、[LINE Developers](https://developers.line.me/ja/)のサイトにアクセスし、WebHookの設定をする。


| 設定項目       |       設定値    |
|:-----------------|:-----------|
| Webhook送信            |    利用する   |
| Webhook URL     |            API Gatewayの「API エンドポイント」   ※1   |
| 自動応答メッセージ   |  利用しない     |
※1.HTTPS のポート番号を指定して設定してください。

```
https://APIGatewayのドメイン名:443/ステージ名/関数名
```

## Lambdaの環境変数設定
Lambdaのコンソール画面に戻り、以下の設定を行います。
※大切なキー情報の流出を防ぐためにも、環境変数に定義しましょう
![スクリーンショット 2019-06-14 21.05.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378268/ab7e4d2a-fb71-936f-1fb5-3141998aa1bf.png)


| キー       |       値    |
|:-----------------|:-----------|
| LINE_CHANNEL_ACCESS_TOKEN            |    アクセストークン   |
| LINE_CHANNEL_SECRET     |   Channel Secret |


## Pythonコード作成
以下を順々に行ないます。
・LINEからのリクエストであることの検証
・リクエストtypeの判別
・LINEへのリプライ
・Slack通知
・Google Apps Scriptの呼び出し

### LINEからのリクエストであることの検証
[公式](https://developers.line.biz/ja/reference/messaging-api/#signature-validation)に書かれていること
>署名を検証する
検証の手順は以下のとおりです。
1.チャネルシークレットを秘密鍵として、HMAC-SHA256アルゴリズムを使用してリクエストボディのダイジェスト値を取得します。
2.ダイジェスト値をBase64エンコードした値とリクエストヘッダーにある署名が一致することを確認します。

図で見て理解しましょう。
WebHookのHeader情報に含まれる「X-Line-Signature」は以下の手順で作られているため、
同じ手順でハッシュ値を求め、突合すれば検証がうまくいきます。

![Untitled Diagram-3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/378268/e427b57c-634b-f627-9402-71ab6aedbbfd.jpeg)

まずは関数の定義

```python:lambda_function.py
# 署名の検証
def validateReq(request):
    # 検証結果
    validateResult = False

    try:
        # Request情報取得
        body = request['body']
        header = request['headers']

        # リクエストBodyのハッシュ化(SHA256)
        hash = hmac.new(LINE_CHANNEL_SECRET.encode('utf-8'),
        body.encode('utf-8'), hashlib.sha256).digest()

        # エンコーディング(base64)
        signature = base64.b64encode(hash).decode('utf-8')

        #検証
        if signature == header['X-Line-Signature'] :
            validateResult = True

    except:
        logger.info(e.args)
        validateResult = False
    finally:
        return validateResult
```

lambda_handlerからの呼び出し

```python:lambda_function.py
def lambda_handler(request, context):

    if not validateReq(request) :
        logger.info("LINE 以外からのアクセス")
        return {'statusCode': 405, 'body': '{}'}

    return {'statusCode': 200, 'body': '{}'}
```
### リクエストtypeの判別
今回は、LINEからのメッセージがtextか画像かの判定のみ行います。
※その他のリクエストtypeについては[公式](https://developers.line.biz/ja/reference/messaging-api/#message-event)を参照

```json:【公式】WebHookリクエストbodyの例
{
  "destination": "xxxxxxxxxx", 
  "events": [
    {
      "replyToken": "0f3779fba3b349968c5d07db31eab56f",
      "type": "message",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U4af4980629..."
      },
      "message": {
        "id": "325708",
        "type": "text",
        "text": "Hello, world"
      }
    },
    {
      "replyToken": "8cf9239d56244f4197887e939187e19e",
      "type": "follow",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U4af4980629..."
      }
    }
  ]
}
```

例から、複数のeventsオブジェクトのtypeを判別していけば良いとわかります。
コードは以下

```python:lambda_function.py

    #eventの回数分繰り返す
    for event in json.loads(request['body'])['events']:

        #typeがtextの場合
        if event["message"]["type"] == "text":
            logger.info("テキストが送られた")

        #typeがimageの場合
        elif event["message"]["type"] == "image":
            logger.info("画像が送られた")

    return {'statusCode': 200, 'body': '{}'}
```

###LINEへのリプライ
以下の関数で、LINEから来たtext情報をおうむ返しできます。

```python:lambda_function.py

# LINEへリプライ
def replyLine(event):

    #リプライ先のURL
    url = 'https://api.line.me/v2/bot/message/reply'

    #POST-Heder作成
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN
    }

    #POST-Body作成
    body = {
        'replyToken': event['replyToken'],
        'messages': [
            {
                "type": "text",
                "text": event['message']['text']
            }
        ]
    }

    #Requestsオブジェクト生成
    req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers)

    #LINEへリプライ
    with urllib.request.urlopen(req) as res:
        logger.info(res.read().decode("utf-8"))

```

### Slack通知
Slackのチャンネルに通知を行います。
Slack側の設定は[ここ](https://qiita.com/yokoc1322/items/553ad147b82277b2beca)が一番わかりやすいです。
簡単なtextを通知するサンプルコードは以下

```python:lambda_function.py
# Slack通知
def notifySlack(text):

    #設定したWebhookのURLを設定してください。
    url = "Slack_WebhookのURL"

    #POSTデータ作成
    send_data = {
        "username": "LINE_BOT",
        "icon_emoji": ":man-heart-man:",
        "text": text
    }
    send_text = "payload=" + json.dumps(send_data)

    #Requestsオブジェクト生成
    req = urllib.request.Request(url,data=send_text.encode('utf-8'),method="POST")

    #Slack通知
    with urllib.request.urlopen(req) as res:
        logger.info(res().decode("utf-8"))
```

POSTリクエストを送るところは、LINEへのリプライ処理と同一のため、共通化しても良いですね！

###Google Apps Scriptの呼び出し
GASの呼び出しを行い、LINEから送られて来た画像をGoogle Driveに保存します。
設定に関しては[ここ](https://qiita.com/rf_p/items/2eeb5e5e863fb3f42860)を参照してください。

####Script環境変数の設定
Scriptの編集ページから、
ファイル > プロジェクトのプロパティを押下し、スクリプトのプロパティから以下を設定してください。

| プロパティ       |       値    |
|:-----------------|:-----------|
| GOOGLE_DRIVE_FOLDER_ID   |  Google Driveの保存先のフォルダID ※1  |
| LINE_CHANNEL_ACCESS_TOKEN    |   LINEのアクセストークン |

※1.フォルダIDはGoogle DriveのフォルダURLの以下の部分です。
```
https://drive.google.com/drive/u/0/folders/フォルダID?ths=true
```

####Scriptの作成
LINEからImageIDをPOSTメソッドで受け取り、
ImageIDをキーに画像をBlob型で取得します。
取得した画像データは、Google Driveに保存します。
※POSTメソッドを受け取る場合は、doPostメソッドを定義してあげましょう。

```コード.gs
// 環境変数取得
var PROPERTIES = PropertiesService.getScriptProperties();

//Google DriveのフォルダIDの取得
var GOOGLE_DRIVE_FOLDER_ID = PROPERTIES.getProperty('GOOGLE_DRIVE_FOLDER_ID')

//LINEアクセストークンの取得
var LINE_ACCESS_TOKEN = PROPERTIES.getProperty('LINE_CHANNEL_ACCESS_TOKEN')

//LINEからPOSTリクエストを受けたときに起動する
function doPost(e){
    //messageIdから、Line上に存在するバイナリ形式の画像URLを取得します
    var json = JSON.parse(e.postData.contents);
    var messageId= json.messageId;
    imageBlob = getImageBlobByImageUrl(messageId);
  
  　//Blob形式のデータをGoogle Driveにアップロード
    imageUrl = saveImageBlobAsPng(imageBlob)
    return imageUrl
 }

//messageIdから、Line上に存在するバイナリ形式の画像データを取得します
function getImageBlobByImageUrl(messageId){
    var url = "https://api.line.me/v2/bot/message/" + messageId + "/content"
    var res = UrlFetchApp.fetch(url, {
      'headers': {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN,
      },
      'method': 'get'
    });

    var imageBlob = res.getBlob().getAs("image/png").setName("temp.png")
    return imageBlob;

}

//Blob形式のデータをGoogle Driveにアップロード
function saveImageBlobAsPng(imageBlob){
    try{
    var folder = DriveApp.getFolderById(GOOGLE_DRIVE_FOLDER_ID);
    var file = folder.createFile(imageBlob);
    return file.getUrl()
  } catch (e){
    Logger.log(e)
  }

}
```

#### GASの呼び出し
Lambda側で以下の関数を定義してあげましょう。

```python:lambda_function.py

#LINE画像をGoogle Driveにアップロードする
def postLineImage(messageId):

    url = "GASのURL"
    body = {
        'messageId': messageId
    }
    headers = {
    }


    #Requestsオブジェクト生成
    req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers)

    #GAS呼び出し
    with urllib.request.urlopen(req) as res:
        logger.info(res.read().decode("utf-8"))

```

##まとめ
全ての関数の呼び出しをまとめたソースは以下です。

```python:lambda_function.py
import logging
import os
import urllib.request, urllib.parse
import json
import base64
import hashlib
import hmac

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# グローバル変数
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
LINE_CHANNEL_SECRET = os.environ['LINE_CHANNEL_SECRET']

def lambda_handler(request, context):

    #リクエストの検証
    if not validateReq(request) :
        logger.info("LINE 以外からのアクセス")
        return {'statusCode': 405, 'body': '{}'}

    #eventの回数分繰り返す
    for event in json.loads(request['body'])['events']:

        #typeがtextの場合
        if event["message"]["type"] == "text":
            #LINEへリプライ
            replyLine(event)
            #Slack通知
            notifySlack(event['message']['text'])

        #typeがimageの場合
        elif event["message"]["type"] == "image":
            #Google Driveへアップロード
            postLineImage(event['message']['id'])

    return {'statusCode': 200, 'body': '{}'}
```