【続】LINEメッセージをAWS Lambdaで送受信してみたお話

## はじめに
業務にてLINE MessagingAPIを利用したLINEでのコミュニケーションツールを作成したので、技術共有と自分の備忘録もかねて書いていきます。
今回は[前回の投稿](https://qiita.com/inacyc_k/items/709f6670d1f0b5ad3677)の追加機能実装となります。以前までのテキストメッセージ以外にも画像の送受信実装を行います。

## 前回の振り返り
前回はLINE公式アカウントを作成し、テキストメッセージの送受信を行いました。
いわゆるこんな感じ。
まずはLINEからユーザーがメッセージを行うと、同じテキストを返信する、というもの。

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/f6d8be72-fc9c-3f92-2f39-5310fd2468f8.png">

そしてAPIを叩くことで、テキストメッセージを送信するです。
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/b2ffc26f-0245-f074-d1e1-a9fc5014b669.png">

ざっとの説明のため、詳しく知りたい、また今回LINE公式アカウント設定などは省略するため、よければぜひ[前回の投稿](https://qiita.com/inacyc_k/items/709f6670d1f0b5ad3677)をご覧ください。

## LINE構成
今回作成する構成は以下のようになります。
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/aec51016-2657-eddb-144d-c6566b251271.png">

前回からの追加はS3バケットを追加したことです。
ユーザーが送信する画像の情報をS3バケットに格納し、中身が確認できるようにする。
そしてAPIからは送信したい画像の情報をS3バケットに格納し、それをトリガーにLINEでユーザーへ送信します。


## 事前準備
前回構成に追加でS3バケットを作成します。
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/c3aebe14-d4fe-95d4-705b-7f71cfe2ef04.png">

お好きなバケット名でバケットを作成します。

:::note info
バケット名はURLの一部にもなるので、AWS内で一意である必要があります。同じ名前で複製等できないのでご注意を！
:::

作成したバケットのセキュリティを変更します。
デフォルトでパブリックアクセスをすべてブロックしているため、そちらを一度解除し、アクセスコントロールリストからのアクセスをブロックにチェックを入れます。
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/9aa6f4a0-32d5-be4f-36cb-cf6e4e9da1d6.png">

次にS3バケットポリシーを以下に設定します。
許可するアクションは、作成したバケットに対し
・”PutObject”…バケットにオブジェクトをアップロードを許可
・”GetObject”…バケット内のオブジェクトをダウンロードを許可
です。
```json:policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:PutObject",
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::MY_BUCKET_NAME/*"
        }
    ]
}
```
S3にフォルダを2つ作成します。
・LINEから取得した画像情報を格納するフォルダ（今回は「line/」）
・LINE公式アカウントから送信したい画像情報を格納するフォルダ（今回は「temp/」）

作成した2つ目のS3フォルダにはLambdaのトリガーとして追加します。
接続したいLambda関数を選択し、”トリガーを追加”を選択します。

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/672fc035-7c68-d291-dfee-5a458fd47864.png">

トリガーの設定では先ほど作成したS3と選択します。
今回手動でS3へ格納するため、イベントタイプはPUTを選択。オプションで格納先フォルダを指定しておきます。

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/a8e0f327-347e-b408-3d49-34b904f45ded.png">
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/d1c09272-7435-7372-c2a4-d4b4b6aefa38.png">

:::note info
設定時にもチェックが必要な項目ですが、PUTをトリガーとしLambda関数が起動するため、異なったフォルダを設定をしたり、Lambda関数内でPUT処理を記述すると処理がループする可能性がありますのでご注意を！
:::

## LINE公式アカウントへメッセージ送信

早速Lambda関数を作成していきます！前回投稿のコード分岐内を一部修正した形での実装です。

```python:Receive.py
import json
import urllib

import boto3
from linebot import (
    LineBotApi,
)
from linebot.models import TextSendMessage,ImageSendMessage

# S3接続情報    
s3_resource = boto3.client('s3')

def lambda_handler(event, context):
    
    # paramater取得
    body = json.loads(event["body"])
    event_type = body["events"][0]["type"]
    user_id = body['events'][0]['source']['userId']
    print(f'body: {body}')
    
    # 環境値（ご自身の環境値に置き換えてください。）
    line_channel_access_token = 'LINE_CHANNEL_ACCESS_TOKEN'

    # メッセージ送信BOT作成
    line_bot_api = LineBotApi(line_channel_access_token)

    # Type分岐
    # メッセージ送信の場合の処理
    if event_type == 'message':
        message_type = body['events'][0]['message']['type']
        reply_token = body['events'][0]['replyToken']

        # 送信されたメッセージがtext（テキスト）の場合の処理
        if message_type == 'text':
            message = body['events'][0]['message']['text']
            line_bot_api.reply_message(reply_token, TextSendMessage(message))
            
        # 送信されたメッセージがimage（画像）の場合の処理
        elif message_type =='image':
        ##---- 追加 ---- 
            message_id = body['events'][0]['message']['id']

            # 環境値（ご自身の環境値に置き換えてください。）
            bucket_name = 'S3_BUCKET_IMAGES'

            # リクエストURLを構築
            url = f"https://api-data.line.me/v2/bot/message/{message_id}/content"
            # リクエストヘッダー
            headers = {
                'Authorization': f'Bearer {line_channel_access_token}'
            }
        
            # HTTP GETリクエストの送信
            try:        
                request = urllib.request.Request(
                    url, headers=headers)
                with urllib.request.urlopen(request) as f:
                    response = f.read()
        
            except Exception:
                # エラーハンドリング
                return {
                    'statusCode': 200,
                    'body': json.dumps({'message': 'システムエラーが発生しました。(LINE通信エラー)'}, ensure_ascii=False)
                }
            
            file_path = 'line/' + message_id + '.jpg'
            
            try:
                # s3格納
                s3_resource.put_object(Bucket=bucket_name, Key=file_path, Body=response)
        
            except Exception:
                # エラーハンドリング
                return {
                    'statusCode': 200,
                    'body': json.dumps({'message': 'システムエラーが発生しました。(バケットエラー)'}, ensure_ascii=False)
                }

            line_bot_api.reply_message(reply_token, ImageSendMessage(original_content_url='https://s3-ap-northeast-1.amazonaws.com/' + bucket_name + '/' + file_path, preview_image_url='https://s3-ap-northeast-1.amazonaws.com/' + bucket_name + '/' + file_path))
            
        # 上記以外のmessage_typeの場合の処理（今回の対象外）
        else:
            pass
               
    # メッセージ送信取消の場合の処理
    elif event_type == 'unsend':
        line_bot_api.push_message(user_id, TextSendMessage("送信取消されました。"))
    
    # 上記以外のevent_typeの場合の処理（今回の対象外）
    else:
        pass
    
    return {
        'statusCode': 200
    }


```

実際の動きはこのような感じです。
<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/2aabecd8-8bb2-40dd-a6d2-e3c305066fe6.png">

テキストメッセージは前回同様におうむ返しですが、画像を送信してみると、、？

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/766b9a8b-39f6-c745-8ae8-1ac2659e435a.jpeg">

送信した画像もおうむ返しされました！（ｵｲｼｿｳ）
またS3のバケットを確認してみると、指定フォルダ配下にオブジェクトが保存されているのも確認できました！

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/41e026c5-6d29-4c4b-ea5a-bc47ef91e9ae.png">

---
具体的なコードの説明を行なっていきます。今回のポイントは以下の箇所。
```python
# リクエストURLを構築
url = f"https://api-data.line.me/v2/bot/message/{message_id}/content"
# リクエストヘッダー
headers = {
    'Authorization': f'Bearer {line_channel_access_token}'
}
```
LINEのMessaging APIを用いてコンテンツを取得します。パスパラメーターにはWebhookで受信したメッセージIDを使っています。こちらは画像以外にも、動画や音声も取得可能です。
正常なメッセージIDの場合200、
存在しないメッセージIDの場合404、
すでに送信取り消しをされたメッセージIDの場合410、のレスポンスとなります。

:::note info
取得できるのはLINEメッセージ送信から30日以内です。
:::

参照：[LINE公式ドキュメント　コンテンツ取得](https://developers.line.biz/ja/reference/messaging-api/#get-content)

---
LINEから送信できるメディアには制限があります。

画像
| 制限項目 |  |
|-----------|------------|
| 画像のURL       | 最大文字数、2000文字  |
| プロトコル     | HTTPS      |
| 画像フォーマット       | JPEGまたはPNG        |
| 画像最大ファイルサイズ       | 10MB（プレビューは1MB）|

動画
| 制限項目 |  |
|-----------|------------|
| 動画のURL       | 最大文字数、2000文字  |
| プロトコル     | HTTPS     |
| 動画フォーマット       |  MP4   |
| 動画最大ファイルサイズ       | 200MB（プレビューは1MB）|

画像動画以外のメディアもLINEから送信が可能です。詳しい制限については以下を参照ください。
参照：[LINE公式ドキュメント　送信メッセージ条件](https://developers.line.biz/ja/reference/messaging-api/#image-message)


## LINE公式アカウントからメッセージ受信

次にLINE公式アカウントからS3バケットに格納した画像情報をユーザーへ送信します。
（今回送信する画像情報は上記記載のLINEメッセージ送信制限を満たしたものとします）

```python:Send.py
from linebot import (
    LineBotApi,
)
from linebot.models import ImageSendMessage


def lambda_handler(event, context):

    # paramater取得
    bucket_name = event['Records'][0]['s3']['bucket']['name']
    file_path = event['Records'][0]['s3']['object']['key']

    # 環境値（ご自身の環境値に置き換えてください。）
    line_channel_access_token = 'LINE_CHANNEL_ACCESS_TOKEN'   
    user_id = 'LINE_UID'
    
    content_url = 'https://s3-ap-northeast-1.amazonaws.com/' + bucket_name + '/' + file_path
    
    # メッセージ送信BOT作成
    line_bot_api = LineBotApi(line_channel_access_token)
    line_bot_api.push_message(user_id, ImageSendMessage(original_content_url= content_url, preview_image_url = content_url))
      
```

実際の動きはこのような感じです。
S3バケットにJPEGファイルをアップロードします。

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/d0d522ed-9497-05b0-a002-a914f15af082.png">

そうすると…？

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/b40885fe-aaf4-12fd-7d1e-d448c92a26c5.png">

S3バケットに格納した画像が受信できました！（ﾒｼﾃﾛ）

# LINE Webhook受信についての注意

今回の実装はLINEのWebhookをAPI Gatewayを通じ、Lamdba関数を実行します。LINEには2秒以内にレスポンスを返す必要があるため、Lambda関数での処理は非同期処理で実装することを推奨しています。
（記載している実装は署名処理を省いていること、処理実行時間が2秒かからないことから非同期処理実装をしておりません。）

<img width="70%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/754925a5-e450-fe41-1868-3959df7bffae.png">

引用：[LINE公式ドキュメント　Webhookリクエスト受信時の処理フロー
](https://developers.line.biz/ja/docs/partner-docs/development-guidelines/#webhook-flow-image)


# まとめ
今回はLINE公式アカウントを通じ、テキストメッセージ以外のメッセージをS3バケットを通じて送受信を行いました。例では画像情報しか実装していませんが、動画や音声などさまざまなメディア形式を送受信可能です。ぜひ試してみてくださいね！
引き続き、AWSおよびLINEについての技術吸収、発信ができればと思います。よろしくお願いします〜！