【入門編②】Serverless Framework で 「じゃんけん」LINE Bot を作る

記事タイトルとURLをコピーする

こんにちは。技術4課の河野です。
今回は、LINEBot 入門編第二弾となります。

入門編第一弾では、「おうむ返し」Botを紹介しました。
第二弾ではよりインタラクティブなBotを目指して、じゃんけんBotを紹介します。

※当記事は、【入門編①】Serverless Framework で 「おうむ返し」LINE Bot を作るを 実施した前提で進めていきます。

作成するもの

下図のイメージ通りで、Bot とじゃんけん対決できます。

f:id:swx-go-kawano:20200907111535p:plain

構成図

構成図は、前回の記事で紹介したものと変わっていません。

f:id:swx-go-kawano:20200903174204p:plain

本記事では、構成図の「AWS Lambda with line-bot-sdk-python」 を変更していきます。

環境

  • Python 3.8.2
  • pipenv
  • serverless Framework Core 1.81.1

実装

各メッセージ処理ごとに解説していきます。
ソースの全容は最後に載せているので、とにかく動かしてみたい方はそちらをご参照ください。

「最初はぐー、」メッセージを表示

f:id:swx-go-kawano:20200907111648p:plain

まず、このメッセージを表示させるイベント(メッセージを表示させるタイミング)について説明します。

  • FollowEvent
    • ユーザーが友達登録した際に発生するイベントです。
    • 通常は「友達登録ありがとうございます...」などの挨拶文やBotの使用方法などを記載しますが、今回はいきなりじゃんけんを仕掛けるようにします。
  • MessageEvent
    • ユーザーがテキストを送信した際に発生するイベントです。
    • 「ぐー」「ちょき」「ぱー」以外のメッセージが送信された場合に表示します。
  • default
    • どのイベントにも属さない場合に発生するイベントです。
    • line-bot-sdk-python で独自に用意されているイベントハンドラーになります。

イベントの詳細については、公式ドキュメントをご参考ください。

developers.line.biz

line-bot-sdk-python では、それぞれのイベントに該当するイベントハンドラーが用意されています。イベントハンドラーは@handler.add(arg1, arg2)のデコレータを追加することで、簡単に実装することができます。 第一引数にイベントの種類、第二引数は、第一引数がMessageEventの場合、そのメッセージの種類(テキスト、写真、スタンプ等)を指定します。指定しない場合は、全てのメッセージの種類が該当することになります。

イベントとメッセージ一覧は、Github をご参照ください。

github.com

まずは、メッセージを表示するJSONファイルを作成します。
saisyohaguu_message.jsonを新規で作成し、下記のように編集します。

{
    "type": "bubble",
    "direction": "ltr",
    "body": {
        "type": "box",
        "layout": "vertical",
        "contents": [
            {
                "type": "text",
                "text": "最初はぐー、じゃんけん、",
                "align": "center"
            }
        ]
    },
    "footer": {
        "type": "box",
        "layout": "horizontal",
        "contents": [
            {
                "type": "button",
                "action": {
                    "type": "message",
                    "label": "ぐー",
                    "text": "ぐー"
                }
            },
        {
            "type": "button",
            "action": {
                "type": "message",
                "label": "ちょき",
                "text": "ちょき"
            }
        },
        {
            "type": "button",
            "action": {
                "type": "message",
                "label": "ぱー",
                "text": "ぱー"
            }
        }
    ]}
}

bot_execute.pyを開いて、下記内容を追記します。

from linebot.models import MessageEvent, TextMessage, TextSendMessage, FollowEvent, FlexSendMessage

@handler.add(FollowEvent)
def handle_follow(event):
    with open('./saisyohaguu_message.json') as f:
        saisyohaguu_message = json.load(f)

    line_bot_api.reply_message(
        event.reply_token,
        FlexSendMessage(alt_text='最初はぐー', contents=saisyohaguu_message)
    )

@handler.default()
def default(event):
    with open('./saisyohaguu_message.json') as f:
        saisyohaguu_message = json.load(f)

    line_bot_api.reply_message(
        event.reply_token,
        FlexSendMessage(alt_text='最初はぐー', contents=saisyohaguu_message)
    )

フォローイベントとデフォルトイベントを実装しました。メッセージイベントは、じゃんけん応答メッセージの表示で合わせて実装します。

「じゃんけんの応答メッセージ」の表示

f:id:swx-go-kawano:20200907111902p:plain

じゃんけんの応答メッセージでは、以下のメッセージを同時に返信します。

  • Bot側の手
  • 勝利判定
  • スタンプ
  • あいこ
  • 次のじゃんけん(最初はぐー)

aikode_message.jsonを新規で作成し、下記のように編集します。

{
    "type": "bubble",
    "direction": "ltr",
    "body": {
        "type": "box",
        "layout": "vertical",
        "contents": [
            {
                "type": "text",
                "text": "あいこで",
                "align": "center"
            }
        ]
    },
    "footer": {
        "type": "box",
        "layout": "horizontal",
        "contents": [
            {
                "type": "button",
                "action": {
                    "type": "message",
                    "label": "ぐー",
                    "text": "ぐー"
                }
            },
        {
            "type": "button",
            "action": {
                "type": "message",
                "label": "ちょき",
                "text": "ちょき"
            }
        },
        {
            "type": "button",
            "action": {
                "type": "message",
                "label": "ぱー",
                "text": "ぱー"
            }
        }
    ]}
}

bot_execute.pyを開いて、下記内容を追記します。

from linebot.models import MessageEvent, TextMessage, TextSendMessage, FollowEvent, FlexSendMessage, StickerSendMessage

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    request_message = event.message.text
    bot_answer = random.choice(['ぐー', 'ちょき', 'ぱー'])

    with open('./saisyohaguu_message.json') as f:
        saisyohaguu_message = json.load(f)
    with open('./aikode_message.json') as f:
        aikode_message = json.load(f)

    reply_messages = []
    win_reply_message = [TextSendMessage(text='私の勝ちです')]
    win_reply_message.append(StickerSendMessage(package_id='1', sticker_id=random.choice(['106', '407', '125', '100', '110'])))
    win_reply_message.append(FlexSendMessage(alt_text='最初はぐー', contents=saisyohaguu_message))
    lose_reply_message = [TextSendMessage(text='私の負けです')]
    lose_reply_message.append(StickerSendMessage(package_id='2', sticker_id=random.choice(['152', '18', '25', '173', '524'])))
    lose_reply_message.append(FlexSendMessage(alt_text='最初はぐー', contents=saisyohaguu_message))
    draw_reply_message = [FlexSendMessage(alt_text='あいこで', contents=aikode_message)]

 if request_message == 'ぐー':
        reply_messages.append(TextSendMessage(text=bot_answer))
        if bot_answer == 'ぐー':
            reply_messages.extend(draw_reply_message)
        elif bot_answer == 'ちょき':
            reply_messages.extend(lose_reply_message)
        elif bot_answer == 'ぱー':
            reply_messages.extend(win_reply_message)
    elif request_message == 'ちょき':
        reply_messages.append(TextSendMessage(text=bot_answer))
        if bot_answer == 'ぐー':
            reply_messages.extend(win_reply_message)
        elif bot_answer == 'ちょき':
            reply_messages.extend(draw_reply_message)
        elif bot_answer == 'ぱー':
            reply_messages.extend(lose_reply_message)
    elif request_message == 'ぱー':
        reply_messages.append(TextSendMessage(text=bot_answer))
        if bot_answer == 'ぐー':
            reply_messages.extend(lose_reply_message)
        elif bot_answer == 'ちょき':
            reply_messages.extend(win_reply_message)
        elif bot_answer == 'ぱー':
            reply_messages.extend(draw_reply_message)
    else:
        reply_messages.append(FlexSendMessage(alt_text='最初はぐー', contents=saisyohaguu_message))

    line_bot_api.reply_message(event.reply_token, reply_messages)

ロジックの説明は省きますが、以下補足します。

  • reply_messageは同時に最大5件まで送信できます。送信時は、リストにメッセージオブジェクトを入れる必要があります。
  • スタンプは、IDの組み合わせを指定して送信します。組み合わせ表はこちらを参照しました。

動作確認

デプロイして、動作を確認します。

sls deploy --stage dev

f:id:swx-go-kawano:20200907125105p:plain

さいごに

これで入門編は以上になります。 次回は外部のAPIを使って、より実用的なBotを紹介できればと思います。

以下、bot_execute.pyの全容です。

import os
import json
import boto3
import logging
import random
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage, FollowEvent, FlexSendMessage, StickerSendMessage

channel_secret = os.getenv('LINE_CHANNEL_SECRET')
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN')

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# LineBotAPI
line_bot_api = LineBotApi(channel_access_token)
handler = WebhookHandler(channel_secret)

sqs = boto3.client('sqs')
queue_url = os.getenv('QUEUE_URL')

# Lambda Response
ok_response = {
    "isBase64Encoded": False,
    "statusCode": 200,
    "headers": {},
    "body": ""
}
error_response = {
    "isBase64Encoded": False,
    "statusCode": 401,
    "headers": {},
    "body": ""
}


@handler.add(FollowEvent)
def handle_follow(event):
    with open('./saisyohaguu_message.json') as f:
        saisyohaguu_message = json.load(f)

    line_bot_api.reply_message(
        event.reply_token,
        FlexSendMessage(alt_text='最初はぐー', contents=saisyohaguu_message)
    )


@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    request_message = event.message.text
    bot_answer = random.choice(['ぐー', 'ちょき', 'ぱー'])

    with open('./saisyohaguu_message.json') as f:
        saisyohaguu_message = json.load(f)
    with open('./aikode_message.json') as f:
        aikode_message = json.load(f)

    reply_messages = []
    win_reply_message = [TextSendMessage(text='私の勝ちです')]
    win_reply_message.append(StickerSendMessage(package_id='1', sticker_id=random.choice(['106', '407', '125', '100', '110'])))
    win_reply_message.append(FlexSendMessage(alt_text='最初はぐー', contents=saisyohaguu_message))
    lose_reply_message = [TextSendMessage(text='私の負けです')]
    lose_reply_message.append(StickerSendMessage(package_id='2', sticker_id=random.choice(['152', '18', '25', '173', '524'])))
    lose_reply_message.append(FlexSendMessage(alt_text='最初はぐー', contents=saisyohaguu_message))
    draw_reply_message = [FlexSendMessage(alt_text='あいこで', contents=aikode_message)]

    if request_message == 'ぐー':
        reply_messages.append(TextSendMessage(text=bot_answer))
        if bot_answer == 'ぐー':
            reply_messages.extend(draw_reply_message)
        elif bot_answer == 'ちょき':
            reply_messages.extend(lose_reply_message)
        elif bot_answer == 'ぱー':
            reply_messages.extend(win_reply_message)
    elif request_message == 'ちょき':
        reply_messages.append(TextSendMessage(text=bot_answer))
        if bot_answer == 'ぐー':
            reply_messages.extend(win_reply_message)
        elif bot_answer == 'ちょき':
            reply_messages.extend(draw_reply_message)
        elif bot_answer == 'ぱー':
            reply_messages.extend(lose_reply_message)
    elif request_message == 'ぱー':
        reply_messages.append(TextSendMessage(text=bot_answer))
        if bot_answer == 'ぐー':
            reply_messages.extend(lose_reply_message)
        elif bot_answer == 'ちょき':
            reply_messages.extend(win_reply_message)
        elif bot_answer == 'ぱー':
            reply_messages.extend(draw_reply_message)
    else:
        reply_messages.append(FlexSendMessage(alt_text='最初はぐー', contents=saisyohaguu_message))

    line_bot_api.reply_message(event.reply_token, reply_messages)


@handler.default()
def default(event):
    with open('./saisyohaguu_message.json') as f:
        saisyohaguu_message = json.load(f)

    line_bot_api.reply_message(
        event.reply_token,
        FlexSendMessage(alt_text='最初はぐー', contents=saisyohaguu_message)
    )


def lambda_handler(event, context):
    try:
        for record in event['Records']:
            record_body = json.loads(record['body'])
            signature = record_body["headers"]["X-Line-Signature"]
            event_body = record_body['body']

        handler.handle(event_body, signature)

    except InvalidSignatureError as e:
        logger.error(e)
        return error_response
    except Exception as e:
        logger.info({'error': e})
        return error_response

    return ok_response