4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LINE Botの自動返信メソッドをLambdaで作る

Posted at

概要

  • AWS LambdaとDynamoDBを使ってLINE Botを作ったので、その備忘録の1つです。
  • この記事では、LINE Botの自動返信メソッドをLambdaで作る手順を説明します。

環境

macOS: 13.5.1
Homebrew: 4.1.4
Node.js: v18.14.1
aws-cli: aws-cli/2.13.7 Python/3.11.4 Darwin/22.6.0 source/arm64 prompt/off

前提条件

サマリー(全体の流れ)

  1. DynamoDBの作成
  2. Lambda用のIAMロールの作成
  3. コンソールでLambdaの作成
  4. Lambda環境変数の設定
  5. Lambdaのコードの実装・デプロイ
  6. API Gatewayの設定

DynamoDBの作成

  • AWSへログインし、DynamoDB -> テーブル -> テーブルの作成 をクリック

image.png

  • テーブル名を入力
    • 今回はLINEのユーザー情報を格納するテーブルなので linebot_user_sample とします。
  • パーティションキーを入力
    • LINE Botを友達登録しているユーザーのユーザーIDを格納するので user_id とします。

image.png

  • テーブル設定は デフォルト設定 とします。

image.png

  • テーブルの作成 をクリックし、テーブル作成します。

image.png

  • 作成されたら、テーブル名をクリックし、ARNをメモっておきます。
    • あとで、IAMロールの作成時に使います。

image.png

Lambda用のIAMロールの作成

  • まず最初に、先ほど作成したDynamoDBのテーブルに対する読み書き権限のポリシーを作成します。
  • IAM -> ポリシー -> 『ポリシーを作成』をクリック

image.png

  • JSON をクリックし、下記のjsonを入力し 次へ をクリック。
    • "Resource" は、先ほど作成したテーブルのarnや、自分のアカウント番号を入力してください。
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:BatchGetItem",
                "dynamodb:GetItem",
                "dynamodb:Query",
                "dynamodb:Scan",
                "dynamodb:BatchWriteItem",
                "dynamodb:PutItem",
                "dynamodb:UpdateItem"
            ],
            "Resource": "arn:aws:dynamodb:ap-northeast-1:999999999999:table/linebot_user_sample"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:ap-northeast-1:999999999999:*"
        },
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "*"
        }
    ]
}

image.png

  • 任意のポリシー名を入力します。今回は MyPolicy_dynamodb_read_write_linebot_sample とします。
    • 説明は空欄でも構いません。
  • ポリシーの作成 をクリックして、作成します。

image.png

  • 一覧を見ると、無事、ポリシーが作成されました。

image.png

  • 次に、LambdaにアタッチするIAMロールの作成をします。
  • IAM -> ロール -> ロールを作成 をクリック。

image.png

  • 信頼されたエンティティタイプ -> AWS のサービス, ユースケース -> Lambda を選択し 次へ をクリック。

image.png

  • 先ほど作成したDynamoDBへの書き込みを許可するロール MyPolicy_dynamodb_read_write_linebot_sample のレ点チェックをつけます。
    • これにより、特定のテーブルに対してのみ、読み書き権限が付与されます。

image.png

  • 次に AWSLambdaBasicExecutionRole のレ点チェックをつけ、次へをクリックします。

image.png

  • 任意のロール名を入力し、ロールを作成をクリックします。
    • 今回のロール名は MyLambdaRole_linebot_sample_dynamodb_read_write とします。

image.png

image.png

  • これでIAMロールの作成は完了です。

コンソールでLambdaの作成

  • Lambda -> 関数 -> 関数の作成 をクリック

image.png

  • 以下の設定にします。
    • 分類
      • 一から作成
    • 関数名
      • なんでもOK(今回は linebot_auto_reply_sample )
    • ランタイム
      • Node.js 18.x
    • アーキテクチャ
      • arm64
    • 実行ロール
      • 既存のロールを使用する
    • 既存のロール
      • 先ほど作成したロール(MyLambdaRole_linebot_sample_dynamodb_read_write)
  • 関数の作成をクリック。

image.png

image.png

  • Lambda関数が作成されました。

image.png

Lambda環境変数の設定

  • シークレットなどを環境変数に登録します。
  • 設定 -> 環境変数 -> 編集をクリック

image.png

  • 3つの環境変数を登録します。
    • ACCESS_TOKEN : LINE Botのチャネルアクセストークン
    • DYNAMODB_TABLE_USER : 先ほど作ったDynamoDBのテーブル名
    • LINE_CHANNEL_SECRET : LINE Botのチャネルシークレット
  • 環境変数の追加 -> コピペ -> 保存をクリック。

image.png

  • これで環境変数の登録完了です。

Lambdaのコードの実装・デプロイ

  • VSCodeを起動し、作業ディレクトリへ移動。
cd hogetaro/my_lambda_project
  • npm初期化を実行
    • 途中の選択肢は、全てデフォルト設定でOKです。(Enter連打でOK)
my_lambda_project/
npm init
my_lambda_project/
npm install @line/bot-sdk
my_lambda_project/
npm install aws-sdk
  • 自動zip化&自動デプロイするために、package.jsonを編集します。
  • "scripts" に以下を追記します。
    • arnの箇所には、先ほど作成したLambda関数のarnを記載してください。
"deploy": "aws lambda update-function-code --function-name arn:aws:lambda:ap-northeast-1:999999999999:function:linebot_auto_reply_sample --zip-file fileb://Lambda-Deployment.zip",
"predeploy": "zip -r Lambda-Deployment.zip * -x *.zip *.json *.log"
  • 解説

    • "deproy"
      • Lambda-Deployment.zip というファイルを arn:aws:lambda:ap-northeast-1:999999999999:function:linebot_auto_reply_sample の関数へアップロードする。
    • "predeploy"
      • "deploy"が実行される前に Lambda-Deployment.zip というzipファイルを作成する。ただし、.zip, .json, .log の拡張子ファイルはzip化しない。(=index.jsとnode_modulesがzip化されます)
  • 修正後のコードは以下の通りです。

my_lambda_project/package.json
{
  "name": "my_lambda_project",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "deploy": "aws lambda update-function-code --function-name arn:aws:lambda:ap-northeast-1:999999999999:function:linebot_auto_reply_sample --zip-file fileb://Lambda-Deployment.zip",
    "predeploy": "zip -r Lambda-Deployment.zip * -x *.zip *.json *.log"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@line/bot-sdk": "^7.5.2",
    "aws-sdk": "^2.1443.0"
  }
}
  • 次に自動でデプロイができるか確認してみます。
  • index.jsを作成します。
my_lambda_project/index.js
// deployテスト

export const handler = async (event) => {
    // TODO implement
    const response = {
      statusCode: 200,
      body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
  };

  • AWS CLI用のIAMユーザーを作業ディレクトリで設定します。
    • 関連記事も参照のこと。
    • dev_cli は適切なユーザー名に修正のこと。
my_lambda_project/
export AWS_DEFAULT_PROFILE=dev_cli
  • デプロイコマンドを実行
    • zip化も行われます。
my_lambda_project/
npm run deploy
  • デプロイできているかは、Lambdaのコンソールを確認すると分かります。
  • 最終更新の時間やコードソースのコメントで、デプロイできていることがわかります。

image.png

  • LINE Bot自動返信機能を実装するために、index.jsを修正します。
my_lambda_project/index.js
const line = require('@line/bot-sdk');
const AWS = require('aws-sdk');

/*
* セキュリティ関係の処理
*/
// LINEからのリクエストかどうかをチェックする
const checkRequestFromLine = (event) => {
    const signature = event.headers["x-line-signature"];
    const body = event.body;
    const channelSecret = process.env.LINE_CHANNEL_SECRET;

    // 署名とbodyをログに出力
    console.log("signature -> ", signature);
    console.log("body -> ", JSON.stringify(body));

    // 署名検証
    const isValid = line.validateSignature(body, channelSecret, signature);
    return isValid;
};


/*
* DynamoDB関係の処理
*/
// ユーザーの投稿テキストを更新する
const updateUserText = async(userId, userText) => {
    const docClient = new AWS.DynamoDB.DocumentClient();

    // 更新するパラメータを設定
    const params = {
        TableName: process.env.DYNAMODB_TABLE_USER,
        Key: {
            user_id: userId
        },
        UpdateExpression: "set user_text = :user_text",
        ExpressionAttributeValues: {
            ":user_text": userText,
        }
    };

    // DBへ書き込み
    docClient.update(params, function(err, data) {
        if(err) {
            console.log("updateUserText() -> error -> ", err);
        } else {
            console.log("updateUserText()成功 -> data -> ", data);
        }
    });
};


/*
* LINE WebHookからのイベント処理メソッド
*/

// textMessageイベントの処理
const textMessageEventReply = async(event) => {
    // 送信先のLINE Bot
    const client = new line.Client({
        channelAccessToken: process.env.ACCESS_TOKEN
    });

    // リプライトークン
    const replyToken = event.replyToken;
    // リプライコンテンツ
    let replyContents = null;

    // テキストメッセージの場合
    if (event.message.type === 'text') {
        // ユーザーの投稿テキスト
        const userText = event.message.text;
        console.log("userText: "+userText);
        replyContents = {
            'type': 'text',
            'text': "あなたの入力した言葉は" + userText + "ですね"
        };

        // ユーザーの投稿テキストを更新する
        const userId = event.source.userId;
        await updateUserText(userId, userText);

    } else {
        const replyText = "ごめんなさい!その種類の投稿には返信できないんです";
        replyContents = {
            'type': 'text',
            'text': replyText
        };
    }

    // クライアントへメッセージ送信
    try {
        await client.replyMessage(replyToken, replyContents);
    } catch(err) {
        console.log("error -> ", err);
    }
};


// postBackイベントの処理
const postBackEventReply = async(event) => {
    // 送信先のLINE Bot
    const client = new line.Client({
        channelAccessToken: process.env.ACCESS_TOKEN
    });

    // postBackイベント内容
    const postBackEventData = event.postback.data;
    console.log("postBackEventData: "+postBackEventData);

    // 返信用トークン
    const replyToken = event.replyToken;

    // リッチメニュー切り替えの場合は返信しない
    if(postBackEventData.includes("richmenu-changed-to")) {
        console.log("リッチメニュー切り替えイベント")
        return
    }

    const replyContents = {
        'type': 'text',
        'text': postBackEventData+" のボタンをタップしましたね!"
    };

    // クライアントへ返信
    try {
        await client.replyMessage(replyToken, replyContents);
    } catch(err) {
        console.log("error -> ", err);
    }
};


// followイベントの処理
const followEventReply = async (event) => {
    // followerのuserId
    const userId = event.source.userId;
    console.log("userId " + userId + " が友達登録しました");

    const docClient = new AWS.DynamoDB.DocumentClient();

    // is_followをtrueに更新する
    const params = {
        TableName: process.env.DYNAMODB_TABLE_USER,
        Key: {
            user_id: userId
        },
        UpdateExpression: "set is_follow = :is_follow",
        ExpressionAttributeValues: {
            ":is_follow": true
        }
    };

    try {
        // DBへ書き込み
        const data = await new Promise((resolve, reject) => {
            docClient.update(params, function(err, responseData) {
                if (err) {
                    reject(err);
                } else {
                    resolve(responseData);
                }
            });
        });
        console.log("followEventReply()成功 -> data -> ", data);
    } catch (err) {
        console.log("followEventReply() -> error -> ", err);
    }
}


// unfollowイベントの処理
const unfollowEventReply = async (event) => {
    // unfollowのuserId
    const userId = event.source.userId;
    console.log("userId " + userId + " が友達登録解除しました");

    const docClient = new AWS.DynamoDB.DocumentClient();

    // is_followをfalseに更新する
    const params = {
        TableName: process.env.DYNAMODB_TABLE_USER,
        Key: {
            user_id: userId
        },
        UpdateExpression: "set is_follow = :is_follow",
        ExpressionAttributeValues: {
            ":is_follow": false
        }
    };

    try {
        // DBへ書き込み
        const data = await new Promise((resolve, reject) => {
            docClient.update(params, function(err, responseData) {
                if (err) {
                    reject(err);
                } else {
                    resolve(responseData);
                }
            });
        });
        console.log("unfollowEventReply()成功 -> data -> ", data);
    } catch (err) {
        console.log("unfollowEventReply() -> error -> ", err);
    }
}




/*
* LINEからのリクエストを処理する
*/
exports.handler = async (event) => {
    console.log('event -> ', event);

    // LINEの疎通確認リクエストの場合は、ステータス200を返す
    // リクエストbodyは {
    //   "destination": "xxxxxxxxxx",
    //   "events": []
    // }
    const requestBody = JSON.parse(event.body);
    if (requestBody.events.length === 0) {
        console.log("LINEの疎通確認リクエストのため、ステータス200を返します");
        return {
            statusCode: 200,
            body: JSON.stringify({
                message: 'LINEの疎通確認リクエストのため、ステータス200を返します',
            }),
            headers: {
                'Content-Type': 'application/json',
            },
        };
    }

    // LINEからのリクエストかどうかをチェックする
    if(!checkRequestFromLine(event)) {
        console.log("LINEからのリクエストではないので、処理を終了します");
        return;
    }

    const eventsArray = requestBody.events;
    console.log("eventsArray -> ", eventsArray);

    // 補足説明: events配列は、イベントによっては複数格納されることがあるので、for文で順番に処理する
    for (const eventItem of eventsArray) {
        // userIdの取得
        const userId = eventItem.source.userId;
        console.log("userId -> ", userId);

        // textMessageイベントの処理
        if (eventItem.type === "message") {
            await textMessageEventReply(eventItem);
        }

        // postBackイベントの処理
        if (eventItem.type === "postback") {
            await postBackEventReply(eventItem);
        }

        // followイベントの処理
        if (eventItem.type === "follow") {
            await followEventReply(eventItem);
        }

        // unfollowイベントの処理
        if (eventItem.type === "unfollow") {
            await unfollowEventReply(eventItem);
        }
    }
};

  • npm run deployでデプロイを忘れずに。

API Gatewayの設定

LINE Botの動作確認

  • 実際にLINE Botにコメントを送信したり、リッチメニューをタップして動作確認をしてみます。
  • 問題なく動作しました。
  • 以上で『LINE Botの自動返信メソッドをLambdaで作る』手順の完了です。
4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?