ChatBot予約システム

内容

LineのChatBotを使った簡易予約システムです。あらかじめ用意されている4つの予約メニュー(空き確認、予約、予約確認、キャンセル)により数タップで予約ができます

想定シーンと利用メリット

このプログラムでは個人レッスンなど予約可能な枠が比較的少ないケースでの使用を想定しています。Lineを予約システムに利用する事で、手間なく簡単に予約の確定や確認、キャンセルがおこなえます

主な利用ツール
  • Python
  • Line Messaging Api
  • AWS api gateway
  • AWS lambda
  • AWS DynamoDB
Code

import os
import sys
import json
import boto3
import datetime

from boto3.dynamodb.conditions import Key, Attr
from time import sleep
secs = 0.7

sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'vendor'))

from linebot import LineBotApi, WebhookHandler,WebhookParser
from linebot.exceptions import InvalidSignatureError
from linebot.models import (CarouselColumn,
                            CarouselTemplate,
                            ConfirmTemplate,
                            ButtonsTemplate,
                            DatetimePickerTemplateAction,
                            MessageEvent,
                            MessageTemplateAction,
                            PostbackTemplateAction,
                            TemplateSendMessage,
                            TextMessage,
                            TextSendMessage,
                            URITemplateAction)
from carousel import (carousel_effect1,
                      carousel_effect2,
                      carousel_effect3,
                      carousel_effect4)

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('********')


def reservable_days_list(x,y,z,da,tr,aw):
    response = table.scan(
        FilterExpression=Attr('reserve_time').between(x,y)
                )
    items = response['Items']
    
    booked_days = set()
    week = ["(月)","(火)","(水)","(木)","(金)","(土)","(日)",]
    for i in items:
        striped_i = i["reserve_time"][:-2]
        booked_day = datetime.datetime.strptime(striped_i,'%Y-%m-%d').strftime('%Y年%-m月%-d日')
        weekday = datetime.datetime.strptime(striped_i,'%Y-%m-%d').weekday()
        booked_days.add(str(booked_day) + week[weekday] + "-" + str(i["reserve_time_n"]))
    
    reserve_term =set()
    today = (datetime.datetime.now())  + datetime.timedelta(days=da)
    
    for t in range(z):
        today = today
        for i in range(len(tr)):
            if not today.weekday() in aw:
                continue
            reserve_term.add("{}{}-{}".format(today.strftime('%Y年%-m月%-d日'),week[today.weekday()] ,i))
        today=(today + datetime.timedelta(days=1))
    reservable_days = sorted(reserve_term.difference(booked_days))
    return reservable_days
    
def multiple_lists(x):
    _list = set()
    for ii in x:
        i ,_ = ii.split("日")
        _list.add(i)
        
    empty_multi_list = [list() for _ in range(len(_list))]
    
    for i in x:
        for n,t in enumerate(_list):
            if i.startswith(t):
                empty_multi_list[n].append(i)
                
    return sorted(empty_multi_list)

def lambda_handler(event, context):
    
    line_bot_api = LineBotApi(os.environ['CHANNEL_ACCESS_TOKEN'])
    handler = WebhookHandler(os.environ['CHANNEL_SECRET'])
    try:
        signature = event['headers']['X-Line-Signature']
    except:
        return {'statusCode': 400, 'body': '{}'}
    body = event["body"]
    
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        return {'statusCode': 400, 'body': '{}'}
    except:
        return {'statusCode': 400, 'body': '{}'}

    body_json = json.loads(body)
    user_id = body_json["events"][0]["source"]["userId"]
    profile = line_bot_api.get_profile(user_id)

    if body_json["events"][0]["type"] == "message":
        message_text = body_json["events"][0]["message"]["text"]
    elif body_json["events"][0]["type"] == "postback":
        if not body_json["events"][0]["postback"]["data"] == "None": 
            message_text = body_json["events"][0]["postback"]["data"]
        try:
            params = body_json["events"][0]["postback"]["params"]
        except:
            pass
 
    days_after = 2
    _from_when = (datetime.datetime.now())  + datetime.timedelta(days=days_after)
    from_when = _from_when.strftime("%Y") + "-" + _from_when.strftime("%m") + "-" + _from_when.strftime("%d") + "-0"
    until_after = 14
    _until_when = (datetime.datetime.now())  + datetime.timedelta(days=until_after)
    until_when = _until_when.strftime("%Y") + "-" + _until_when.strftime("%m") + "-" + _until_when.strftime("%d") + "-0"
    days_between = (_until_when - _from_when).days
    _time_ranges = {"0":"19時から20時","1":"20時から21時"}
    _transform_time = {'19時から20時':0,'20時から21時':1}
    acceptable_weekday = [3,4]
    
    if message_text == "予約する":
        reservable_days = reservable_days_list(x=from_when,y=until_when,z=days_between,da=days_after,tr=_time_ranges,aw=acceptable_weekday)
        reservable_days_multi_list = multiple_lists(reservable_days)

        if len(reservable_days_multi_list) == 1:
            objects = carousel_effect1(x=reservable_days_multi_list,y=_time_ranges,z="します")
        elif len(reservable_days_multi_list) == 2:
            objects = carousel_effect2(x=reservable_days_multi_list,y=_time_ranges,z="します")
        elif len(reservable_days_multi_list) == 3:
            objects = carousel_effect3(x=reservable_days_multi_list,y=_time_ranges,z="します")
        elif len(reservable_days_multi_list) == 4:
            objects = carousel_effect4(x=reservable_days_multi_list,y=_time_ranges,z="します")
        elif len(reservable_days_multi_list) == 0:
            line_bot_api.reply_message(
                    body_json["events"][0]["replyToken"],
                    TextSendMessage(text="申し訳ありませんが現在全ての日程が予約済みです\uDBC0\uDC94 \n後日改めて空き状況をご確認ください\uDBC0\uDC9D ")
                )
        
        line_bot_api.reply_message(
            body_json["events"][0]["replyToken"],
            objects
        )
            
    elif message_text == "空きを確認する":
        reservable_days = reservable_days_list(x=from_when,y=until_when,z=days_between,da=days_after,tr=_time_ranges,aw=acceptable_weekday)
        if not reservable_days:
            line_bot_api.reply_message(
                    body_json["events"][0]["replyToken"],
                    TextSendMessage(text="申し訳ありませんが現在全ての日程が予約済みです\uDBC0\uDC94 \n後日改めて空き状況をご確認ください\uDBC0\uDC9D ")
                )
        else:
            text = ""
            time_ranges = _time_ranges
            for i in reservable_days:
                d,t = i.split("-")
                time_range = time_ranges[t]
                text += d[5:] + " " + time_range + "\n"
            
            line_bot_api.reply_message(
                    body_json["events"][0]["replyToken"],
                    TextSendMessage(text="2週間後までの予約可能日時は以下のとおりです。\nご予約お待ちしております\uDBC0\uDC80\n\n【予約可能日】\n{}".format(text))
                )
    elif message_text == "予約を確認する":
        response = table.query(
        KeyConditionExpression=Key('user_id').eq(user_id) & Key('reserve_time').between(from_when,until_when)
        )
        items = response['Items']
        if not items:
            line_bot_api.reply_message(
                    body_json["events"][0]["replyToken"],
                    TextSendMessage(text="{}さんからはまだご予約頂いておりません\uDBC0\uDC95".format(profile.display_name))
                )
        else:
            booked_days = set()
            week = ["(月)","(火)","(水)","(木)","(金)","(土)","(日)",]
            for i in items:
                striped_i = i["reserve_time"][:-2]
                booked_day = datetime.datetime.strptime(striped_i,'%Y-%m-%d').strftime('%Y年%-m月%-d日')
                weekday = datetime.datetime.strptime(striped_i,'%Y-%m-%d').weekday()
                booked_days.add(str(booked_day) + week[weekday] + "-" + str(i["reserve_time_n"]))
            text = ""
            time_ranges = _time_ranges
            for i in sorted(booked_days):
                d,t = i.split("-")
                time_range = time_ranges[t]
                text += d[5:] + " " + time_range + "\n"
            
            line_bot_api.reply_message(
                    body_json["events"][0]["replyToken"],
                    TextSendMessage(text="{}さんからは現在{}件の予約を頂いております\uDBC0\uDC2D\n予約日時は以下のとおりです\uDBC0\uDC78\n\n【予約日】\n{}".format(profile.display_name,len(booked_days),text))
                )    
        
    elif message_text == "予約をキャンセルする":
        response = table.query(
        KeyConditionExpression=Key('user_id').eq(user_id) & Key('reserve_time').between(from_when,until_when)
        )
        items = response['Items']
        if not items:
            line_bot_api.reply_message(
                    body_json["events"][0]["replyToken"],
                    TextSendMessage(text="{}さんからはまだご予約頂いておりません\uDBC0\uDC95".format(profile.display_name))
                )

        else:
            booked_days = set()
            week = ["(月)","(火)","(水)","(木)","(金)","(土)","(日)",]
            for i in items:
                striped_i = i["reserve_time"][:-2]
                booked_day = datetime.datetime.strptime(striped_i,'%Y-%m-%d').strftime('%Y年%-m月%-d日')
                weekday = datetime.datetime.strptime(striped_i,'%Y-%m-%d').weekday()
                booked_days.add(str(booked_day) + week[weekday] + "-" + str(i["reserve_time_n"]))
            
            cansel_days_multi_list = multiple_lists(booked_days)
            
            if len(cansel_days_multi_list) == 1:
                objects = carousel_effect1(x=cansel_days_multi_list,y=_time_ranges,z="キャンセルします")
            elif len(cansel_days_multi_list) == 2:
                objects = carousel_effect2(x=cansel_days_multi_list,y=_time_ranges,z="キャンセルします")
            elif len(cansel_days_multi_list) == 3:
                objects = carousel_effect3(x=cansel_days_multi_list,y=_time_ranges,z="キャンセルします")
            elif len(cansel_days_multi_list) == 4:
                objects = carousel_effect4(x=cansel_days_multi_list,y=_time_ranges,z="キャンセルします")

            line_bot_api.reply_message(
                body_json["events"][0]["replyToken"],
                [
                TextSendMessage(text="予約のキャンセルですね"),
                objects,
                ]
            )

    elif "の予約をキャンセルしたいです" in message_text:
        message_text = message_text[:-5] + "します"
        objects = TemplateSendMessage(
                        alt_text='Confirm template',
                        template=ConfirmTemplate(
                            text='本当にキャンセルしますか?',
                            actions=[
                                PostbackTemplateAction(
                                    label='はい',
                                    text=message_text,
                                    data='None'
                                ),
                                PostbackTemplateAction(
                                    label='いいえ',
                                    text='いいえ',
                                    data='None'
                                )
                            ]
                        )
                    )
        sleep(secs)
        line_bot_api.reply_message(
            body_json["events"][0]["replyToken"],
            objects
        )
    elif "の予約をキャンセルします" in message_text:
        try:
            transform_time = _transform_time
            changed_date,changed_time = message_text.replace("年","-").replace("月","-").replace("の予約をキャンセルします","").split("日")
            changed_date = datetime.datetime.strptime(changed_date, "%Y-%m-%d").strftime("%Y-%m-%d")
            reserve_time_n = transform_time[changed_time[3:]]
            changed_date = changed_date + "-" + str(reserve_time_n)
            
            table.delete_item(
                Key={'user_id': user_id,
                     'reserve_time': changed_date},
                ConditionExpression=Attr('reserve_time_n').eq(reserve_time_n)
            )
            
            sleep(secs)
            line_bot_api.reply_message(
                    body_json["events"][0]["replyToken"],
                    TextSendMessage(text="キャンセルしましたぁ\uDBC0\uDC7C")
                )
        except:
            return {'statusCode': 400, 'body': '{}'}
    elif "の予約をしたいです" in message_text:
        message_text = message_text[:-5] + "します"
        objects = TemplateSendMessage(
                        alt_text='Confirm template',
                        template=ConfirmTemplate(
                            text='予約してよろしいですか?',
                            actions=[
                                PostbackTemplateAction(
                                    label='はい',
                                    text=message_text,
                                    data='None'
                                ),
                                PostbackTemplateAction(
                                    label='いいえ',
                                    text='いいえ',
                                    data='None'
                                )
                            ]
                        )
                    )
                    
        line_bot_api.reply_message(
            body_json["events"][0]["replyToken"],
            objects
        )
    elif "の予約をします" in message_text:
        try:
            transform_time = _transform_time
            changed_date,changed_time = message_text.replace("年","-").replace("月","-").replace("の予約をします","").split("日")
            changed_date = datetime.datetime.strptime(changed_date, "%Y-%m-%d").strftime("%Y-%m-%d")
            reserve_time_n = transform_time[changed_time[3:]]
            changed_date = changed_date + "-" + str(reserve_time_n)
  
            table.put_item(
               Item={'user_id': user_id,
                     'reserve_time': changed_date,
                     'reserve_time_n': reserve_time_n,
                     'reserve_name': profile.display_name,
                     'reserve_id': 5}
            )
            
            sleep(secs)
            reply_text = message_text[5:-4]
            line_bot_api.reply_message(
                    body_json["events"][0]["replyToken"],
                    TextSendMessage(text="ご予約ありがとうございます\uDBC0\uDC79。\n{}を承りました。\uDBC0\uDCB1".format(reply_text))
                )
        except:
            return {'statusCode': 400, 'body': '{}'}
    elif message_text == "None":
        line_bot_api.reply_message(
                body_json["events"][0]["replyToken"],
                TextSendMessage(text="エラー。もう一度やり直してください\uDBC0\uDC83")
            )
    else:
        line_bot_api.reply_message(
                body_json["events"][0]["replyToken"],
                TextSendMessage(text="予約メニューから選択してください\uDBC0\uDC86")
            )

######### other file ###########

import os
import sys

sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'vendor'))

from linebot.models import (CarouselColumn,
                            CarouselTemplate,
                            ConfirmTemplate,
                            ButtonsTemplate,
                            DatetimePickerTemplateAction,
                            MessageEvent,
                            MessageTemplateAction,
                            PostbackTemplateAction,
                            TemplateSendMessage,
                            TextMessage,
                            TextSendMessage,
                            URITemplateAction)
                            
def transform_reservable_days(x,y):
    transform_reservable_days = []
    time_ranges = y
    for i in x:
        d,t = i.split("-")
        time_range = time_ranges[t]
        transform_reservable_days.append(d + time_range)
    return sorted(transform_reservable_days)

def carousel_effect1(x,y,z):
    reserve_actions_list1 = []
    if z == "します":
        z = "したいです"
        label_text = '予約する時間を選択して下さい'
    elif z == "キャンセルします":
        z = "キャンセルしたいです"
        label_text = 'キャンセルする時間を選択して下さい'
    else:
        pass
        
    ####reserve_actions_list1####
    for i in transform_reservable_days(x[0],y):
        _, label_message= i.split(")")
        reserve_actions_list1.append(PostbackTemplateAction(label='{}'.format(label_message),text='{}の予約を{}'.format(i,z),data='None'))
        _title_message, _ = i.split(")")
        title_message = _title_message.replace("(","")+"曜日"
    
    objects = TemplateSendMessage(
                alt_text='Buttons template',
                template=ButtonsTemplate(
                    thumbnail_image_url=None,
                    title=title_message[5:],
                    text=label_text,
                    actions = reserve_actions_list1
                )
            )
    return objects
    
def carousel_effect2(x,y,z):
    max_variable = max([len(i) for i in x])
    reserve_actions_list1 = []
    reserve_actions_list2 = []
    
    if z == "します":
        z = "したいです"
        label_text = '予約する時間を選択して下さい'
    elif z == "キャンセルします":
        z = "キャンセルしたいです"
        label_text = 'キャンセルする時間を選択して下さい'
    else:
        pass
    
    ####reserve_actions_list1####
    for i in transform_reservable_days(x[0],y):
        _, label_message= i.split(")")
        reserve_actions_list1.append(PostbackTemplateAction(label='{}'.format(label_message),text='{}の予約を{}'.format(i,z),data='None'))
        _title_message, _ = i.split(")")
        title_message = _title_message.replace("(","")+"曜日"
    
    if len(x[0]) < max_variable:
        for _ in range(max_variable-len(x[0])):
            reserve_actions_list1.append(PostbackTemplateAction(label='-',text='None',data='None'))
    
    ####reserve_actions_list1####            
    for i in transform_reservable_days(x[1],y):
        _, label_message= i.split(")")
        reserve_actions_list2.append(PostbackTemplateAction(label='{}'.format(label_message),text='{}の予約を{}'.format(i,z),data='None'))
        _title_message2, _ = i.split(")")
        title_message2 = _title_message2.replace("(","")+"曜日"
    if len(x[1]) < max_variable:
        for _ in range(max_variable-len(x[1])):
            reserve_actions_list2.append(PostbackTemplateAction(label='-',text='None',data='None'))
    
    objects = TemplateSendMessage(
        alt_text='Carousel template',
        template=CarouselTemplate(
            columns=[
                CarouselColumn(
                    thumbnail_image_url = None,
                    title=title_message[5:],
                    text=label_text,
                    actions = reserve_actions_list1
                ),
                CarouselColumn(
                    thumbnail_image_url = None,
                    title=title_message2[5:],
                    text=label_text,
                    actions = reserve_actions_list2
                )
            ]
        )
    )
    return objects
    
def carousel_effect3(x,y,z):
    max_variable = max([len(i) for i in x])
    
    reserve_actions_list1 = []
    reserve_actions_list2 = []
    reserve_actions_list3 = []
    
    if z == "します":
        z = "したいです"
        label_text = '予約する時間を選択して下さい'
    elif z == "キャンセルします":
        z = "キャンセルしたいです"
        label_text = 'キャンセルする時間を選択して下さい'
    else:
        pass
        
    ####reserve_actions_list1####
    for i in transform_reservable_days(x[0],y):
        _, label_message= i.split(")")
        reserve_actions_list1.append(PostbackTemplateAction(label='{}'.format(label_message),text='{}の予約を{}'.format(i,z),data='None'))
        _title_message, _ = i.split(")")
        title_message = _title_message.replace("(","")+"曜日"
    if len(x[0]) < max_variable:
        for _ in range(max_variable-len(x[0])):
            reserve_actions_list1.append(PostbackTemplateAction(label='-',text='None',data='None'))
    
    ####reserve_actions_list2####            
    for i in transform_reservable_days(x[1],y):
        _, label_message= i.split(")")
        reserve_actions_list2.append(PostbackTemplateAction(label='{}'.format(label_message),text='{}の予約を{}'.format(i,z),data='None'))
        _title_message2, _ = i.split(")")
        title_message2 = _title_message2.replace("(","")+"曜日"
    if len(x[1]) < max_variable:
        for _ in range(max_variable-len(x[1])):
            reserve_actions_list2.append(PostbackTemplateAction(label='-',text='None',data='None'))
    
    ####reserve_actions_list3####            
    for i in transform_reservable_days(x[2],y):
        _, label_message= i.split(")")
        reserve_actions_list3.append(PostbackTemplateAction(label='{}'.format(label_message),text='{}の予約を{}'.format(i,z),data='None'))
        _title_message3, _ = i.split(")")
        title_message3 = _title_message3.replace("(","")+"曜日"
    if len(x[2]) < max_variable:
        for _ in range(max_variable-len(x[2])):
            reserve_actions_list3.append(PostbackTemplateAction(label='-',text='None',data='None'))

    objects = TemplateSendMessage(
        alt_text='Carousel template',
        template=CarouselTemplate(
            columns=[
                CarouselColumn(
                    thumbnail_image_url = None,
                    title=title_message[5:],
                    text=label_text,
                    actions = reserve_actions_list1
                ),
                CarouselColumn(
                    thumbnail_image_url = None,
                    title=title_message2[5:],
                    text=label_text,
                    actions = reserve_actions_list2
                ),
                CarouselColumn(
                    thumbnail_image_url = None,
                    title=title_message3[5:],
                    text=label_text,
                    actions = reserve_actions_list3
                )
            ]
        )
    )
    return objects
    
def carousel_effect4(x,y,z):
    max_variable = max([len(i) for i in x])
    
    reserve_actions_list1 = []
    reserve_actions_list2 = []
    reserve_actions_list3 = []
    reserve_actions_list4 = []
    
    if z == "します":
        z = "したいです"
        label_text = '予約する時間を選択して下さい'
    elif z == "キャンセルします":
        z = "キャンセルしたいです"
        label_text = 'キャンセルする時間を選択して下さい'
    else:
        pass
        
    ####reserve_actions_list1####
    for i in transform_reservable_days(x[0],y):
        _, label_message= i.split(")")
        reserve_actions_list1.append(PostbackTemplateAction(label='{}'.format(label_message),text='{}の予約を{}'.format(i,z),data='None'))
        _title_message, _ = i.split(")")
        title_message = _title_message.replace("(","")+"曜日"
    if len(x[0]) < max_variable:
        for _ in range(max_variable-len(x[0])):
            reserve_actions_list1.append(PostbackTemplateAction(label='-',text='None',data='None'))
    
    ####reserve_actions_list2####            
    for i in transform_reservable_days(x[1],y):
        _, label_message= i.split(")")
        reserve_actions_list2.append(PostbackTemplateAction(label='{}'.format(label_message),text='{}の予約を{}'.format(i,z),data='None'))
        _title_message2, _ = i.split(")")
        title_message2 = _title_message2.replace("(","")+"曜日"
    if len(x[1]) < max_variable:
        for _ in range(max_variable-len(x[1])):
            reserve_actions_list2.append(PostbackTemplateAction(label='-',text='None',data='None'))
    
    ####reserve_actions_list3####            
    for i in transform_reservable_days(x[2],y):
        _, label_message= i.split(")")
        reserve_actions_list3.append(PostbackTemplateAction(label='{}'.format(label_message),text='{}の予約を{}'.format(i,z),data='None'))
        _title_message3, _ = i.split(")")
        title_message3 = _title_message3.replace("(","")+"曜日"
    if len(x[2]) < max_variable:
        for _ in range(max_variable-len(x[2])):
            reserve_actions_list3.append(PostbackTemplateAction(label='-',text='None',data='None'))

    ####reserve_actions_list3####            
    for i in transform_reservable_days(x[3],y):
        _, label_message= i.split(")")
        reserve_actions_list4.append(PostbackTemplateAction(label='{}'.format(label_message),text='{}の予約を{}'.format(i,z),data='None'))
        _title_message4, _ = i.split(")")
        title_message4 = _title_message4.replace("(","")+"曜日"
    if len(x[3]) < max_variable:
        for _ in range(max_variable-len(x[3])):
            reserve_actions_list4.append(PostbackTemplateAction(label='-',text='None',data='None'))

    objects = TemplateSendMessage(
        alt_text='Carousel template',
        template=CarouselTemplate(
            columns=[
                CarouselColumn(
                    thumbnail_image_url = None,
                    title=title_message[5:],
                    text=label_text,
                    actions = reserve_actions_list1
                ),
                CarouselColumn(
                    thumbnail_image_url = None,
                    title=title_message2[5:],
                    text=label_text,
                    actions = reserve_actions_list2
                ),
                CarouselColumn(
                    thumbnail_image_url = None,
                    title=title_message3[5:],
                    text=label_text,
                    actions = reserve_actions_list3
                ),
                CarouselColumn(
                    thumbnail_image_url = None,
                    title=title_message4[5:],
                    text=label_text,
                    actions = reserve_actions_list4
                )
            ]
        )
    )
    return objects

お仕事のご依頼はcontactからどうぞ