Closed6

オウム返しするLineBotを作ってみた

yorosuyorosu

ネットサーフィンをした中で最も新しそうだった以下の記事を参考にオウム返ししてみます
https://qiita.com/h-kiyo/items/ef9b832131a4ad76fac3

502 BadGatewayを吐いて、WebhookのVerify(検証)に失敗したので、

CloudWatchでLambdaのログを見てみた

エラー内容を見たところ、どうやらLambdaをアップロードしたzipファイルのディレクトリ構成がよくない気がしたので、.zip直下にlambda_function.pyが来るように見直した
.zip直下にlambda_function.pyが来るようにすること

Lambdaを再アップロードしたら、解決!

yorosuyorosu

Lambdaレイヤーを使ってみた

このあたりの記事を参考にさせていただいた
https://qiita.com/DASHi/items/b03d3efd401260d8add4

環境

Windows11

手順

以下、手順(備忘録)

ライブラリをインストールしてZIP化

レイヤーにしたいライブラリ(今回だとline-bot-sdk)を再度インストールしてZIP化
ライブラリは /python フォルダ内に配置する!
(少しハマッた)

mkdir python
cd python
pip install line-bot-sdk -t .
cd ../
Compress-Archive -Path python -DestinationPath .\line-bot-sdk.zip

レイヤーをアップロード

作ったZipをレイヤーとしてアップロードする

レイヤーをLambda関数に追加

yorosuyorosu

LineBotApiのpush_message()

push_message()の使用例

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

line_bot_api = LineBotApi(channel_access_token)

// テキストの送信
line_bot_api.push_message(USER_ID, TextSendMessage(text='hoge'))
line_bot_api.push_message(USER_ID, ImageSendMessage(
            original_content_url=presigned_url, preview_image_url=presigned_url))

USER_IDは、チャネルに割り当てられているユーザーIDをコピーして格納すること。

APIについて

APIの詳細は、下記のline-bot-sdk-python
https://github.com/line/line-bot-sdk-python

MessagingAPIの説明のページを参照
https://github.com/line/line-bot-sdk-python/blob/master/linebot/v3/messaging/docs/MessagingApi.md#push_message

https://github.com/line/line-bot-sdk-python/blob/master/linebot/models/send_messages.py

yorosuyorosu

S3署名付きURLをコマンドラインで発行してLineで送信

S3署名付きURLを発行

Pythonでこう書くみたいだからLambdaに書いてみた

# S3署名付きURLの発行
presigned_url = s3.generate_presigned_url(
    ClientMethod = 'get_object',
    Params = {'Bucket' : BUCKET_NAME, 'Key' : object_name},
    ExpiresIn = 60,
    HttpMethod = 'GET'
)

以下の記事を参考にしました。
https://qiita.com/kei_japan/items/00266727b64e1167c4f3

エラー

Lambdaで実行して、署名付きURLを発行してもなんか画像見れない・・・

S3オブジェクトの読み取り権限のあるIAMロールを作成、Lambdaに付与

調べてると、上のエラーの原因は、LambdaにS3の読み取り権限がないことらしい。

以下のようなS3の読み取り権限を付けるIAMポリシーを作成。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::parroting/*"
            ]
        }
    ]
}

CloudWatchに対する権限を付与するIAMポリシーも作成しておく。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:ap-northeast-1:081507884114:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:ap-northeast-1:081507884114:log-group:/aws/lambda/parroting-line-bot-function:*"
            ]
        }
    ]
}

作成した2つのIAMポリシーを割り当てたIAMロールを作成。
LambdaにIAMロールを付与。

以下の記事を参考にしました

https://qiita.com/cloud-solution/items/63a200391c8ce8fcdd0a

yorosuyorosu

SSMからLineのパラメータを取得してLambdaのPythonで使用

以下の記事を基本的に参考にした

https://qiita.com/NoOne/items/810b96e878b45ff43f21

SSMでパラメータの作成

記事で紹介されているのと異なる点:
SecureString型で暗号化したかったので、そのように設定
作成時にはデフォルトのKMSキーを指定した。

以下のように作成完了した

IAMポリシーの作成

LambdaからSSMのパラメータを取得するためのポリシーを作成。
作成後Lambdaの関数ロールにアタッチ

記事と異なる点:
"kms:Decrypt"を追加

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "VisualEditor0",
			"Effect": "Allow",
			"Action": [
				"ssm:GetParameters",
				"kms:Decrypt"
			],
			"Resource": "*"
		}
	]
}
yorosuyorosu

完成!

こんな感じで返ってくる!

構成図

LambdaのPythonコード

仕事じゃないので汚いコードでも動けばよいのである。

import os
import sys
import logging

import boto3

from botocore.client import Config

from linebot import (LineBotApi, WebhookHandler)
from linebot.models import (MessageEvent, TextMessage, TextSendMessage, ImageSendMessage)
from linebot.exceptions import (LineBotApiError, InvalidSignatureError)

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

# SSMから複数のパラメータを取得し辞書形式で返す
def get_ssm_params(*keys, region='ap-northeast-1'):
    result = {}
    ssm = boto3.client('ssm', region)
    response = ssm.get_parameters(
        Names=keys,
        WithDecryption=True,
    )
    for p in response['Parameters']:
        result[p['Name']] = p['Value']
    return result


# 複数のパラメータをSSM取得
ssm_params = get_ssm_params('LINE_CHANNEL_SECRET', 'LINE_CHANNEL_ACCESS_TOKEN', 'LINE_BOT_USER_ID')

#LINEBOTと接続するための記述
#LINEBotのチャンネルアクセストークンとシークレットを読み込む
channel_secret = ssm_params['LINE_CHANNEL_SECRET']
channel_access_token = ssm_params['LINE_CHANNEL_ACCESS_TOKEN']
#LineBotのUser_idを読み込む
line_bot_user_id = ssm_params['LINE_BOT_USER_ID']

#無いならエラー
if channel_secret is None:
    logger.error('Specify LINE_CHANNEL_SECRET as environment variable.')
    sys.exit(1)
if channel_access_token is None:
    logger.error('Specify LINE_CHANNEL_ACCESS_TOKEN as environment variable.')
    sys.exit(1)

#apiとhandlerの生成(チャンネルアクセストークンとシークレットを渡す)
line_bot_api = LineBotApi(channel_access_token)
handler = WebhookHandler(channel_secret)

#オウムの画像を格納しているバケット名
BUCKET_NAME = 'parroting'

#S3クライアントの生成
s3 = boto3.client('s3', region_name='ap-northeast-1', config=Config(signature_version='s3v4'))

#Lambdaのメインの動作
def lambda_handler(event, context):
    print("Event:", event)
    # 認証用のx-line-signatureヘッダー
    if "headers" in event and "x-line-signature" in event["headers"]:
        signature = event["headers"]["x-line-signature"]
    else:
        # エラーレスポンスを返す
        return {
            "isBase64Encoded": False,
            "statusCode": 400,
            "headers": {},
            "body": "Missing x-line-signature header"
        }
    body = event["body"]
    # リターン値の設定
    ok_json = {
        "isBase64Encoded": False,
        "statusCode": 200,
        "headers": {},
        "body": ""
    }
    error_json = {
        "isBase64Encoded": False,
        "statusCode": 500,
        "headers": {},
        "body": "Error"
    }

    #メッセージを受け取る・受け取ったら受け取ったテキストを返信する
    @handler.add(MessageEvent, message=TextMessage)
    def message(line_event):
        print("Line_Event:",line_event)
        
        # タイムスタンプ
        timestamp: int = line_event.timestamp
        # メッセージID
        id = line_event.message.id

        # タイムスタンプとメッセージIDを見て送信する写真と送信メッセージを決定
        rem19_time = timestamp % 19
        # 受信したメッセージテキストを送信メッセージにそのまま採用(オウム返し)
        reply_text = line_event.message.text
        # 送信する画像の名前を決定
        object_name = str(rem19_time) + ".jpg"
        preview_object_name = str(rem19_time) + "_preview.jpg"
        
        # メッセージのリプライを実行
        line_bot_api.reply_message(line_event.reply_token, TextSendMessage(text=reply_text))
        
        # S3署名付きURLの発行
        presigned_url = s3.generate_presigned_url(
            ClientMethod = 'get_object',
            Params = {'Bucket' : BUCKET_NAME, 'Key' : object_name},
            ExpiresIn = 60,
            HttpMethod = 'GET'
        )
        # S3署名付きURLの発行(プレビュー用)
        preview_presigned_url = s3.generate_presigned_url(
            ClientMethod = 'get_object',
            Params = {'Bucket' : BUCKET_NAME, 'Key' : preview_object_name},
            ExpiresIn = 60,
            HttpMethod = 'GET'
        )
        
        # S3署名付きURLで写真のリンクを送る
        line_bot_api.push_message(line_bot_user_id, ImageSendMessage(
            original_content_url=presigned_url, 
            preview_image_url=preview_presigned_url))

    #例外処理としての動作
    try:
        handler.handle(body, signature)
    except LineBotApiError as e:
        logger.error("Got exception from LINE Messaging API: %s\n" % e.message)
        for m in e.error.details:
            logger.error("  %s: %s" % (m.property, m.message))
        return error_json
    except InvalidSignatureError:
        return error_json

    return ok_json


このスクラップは1ヶ月前にクローズされました