シンプルなLINEBotアプリを作れるようになったので、次のステップに進みたい!
そんな思いで作ったLINEBotについて紹介します。
素人のコードを貼ってる上に、これといった解説もしてないので、閲覧注意です。
批判は受け付けませんので自己責任で読んでください笑
使っている技術は、
・Python
・Flask
・LINEMessageingAPI
・Heroku
・GoogleSpreadsheetAPI
です。
以前勉強した、Python×スプレッドシートAPIでユーザーIDの保存や、Python初心者がたったの3日間でチャットボットを作った【1日目】のあたりの知識を使っています。
もくじ
今回作ったBotの概要
まず、今回作ったBotについて紹介します。
完成形はこちら。
というbotアプリを作ったので、ぜひテスターとして参加してくれたら嬉しいです…!! pic.twitter.com/9rTWWfDYhN
— カシワバユキ@勉強用 (@yuki_kashiwaba) 2018年11月12日
コンセプトは、「知り合いには恥ずかしくて聞けないような、心と身体の悩みや疑問を見知らぬ異性に聞けるBot」です。
機能は、
・性別の登録
・質問の投稿
・異性の質問に回答
・自分の質問への回答の取得
の4つを実装しました。
作成期間は二日間。
かかった時間は大体5〜6時間くらいです。
LINEMessageingAPIはフリープランを利用し、DBとしてGoogleSpreadsheet、サーバーにHerokuを使用しました。
今はもう運用していないのですが、記録としてコードだけ残しておこうと思います。
メイン部分
先にコード貼っておきます。
ちょっと長いです。
from flask import Flask, request, abort, render_template,redirect from linebot import ( LineBotApi, WebhookHandler ) from linebot.exceptions import ( InvalidSignatureError ) from linebot.models import ( MessageEvent, TextMessage, TextSendMessage, SourceUser, SourceGroup, SourceRoom, TemplateSendMessage, ConfirmTemplate, MessageAction, ButtonsTemplate, ImageCarouselTemplate, ImageCarouselColumn, URIAction, PostbackAction, DatetimePickerAction, CameraAction, CameraRollAction, LocationAction, CarouselTemplate, CarouselColumn, PostbackEvent, StickerMessage, StickerSendMessage, LocationMessage, LocationSendMessage, ImageMessage, VideoMessage, AudioMessage, FileMessage, UnfollowEvent, FollowEvent, JoinEvent, LeaveEvent, BeaconEvent, FlexSendMessage, BubbleContainer, ImageComponent, BoxComponent, TextComponent, SpacerComponent, IconComponent, ButtonComponent, SeparatorComponent, QuickReply, QuickReplyButton ) import os import json import users_DB import questions #アクセスキーの取得 app = Flask(__name__) #BOTの認証。Heroku環境で設定済み。 YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"] YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"] line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN) handler = WebhookHandler(YOUR_CHANNEL_SECRET) @app.route("/callback", methods=['POST']) def callback(): signature = request.headers['X-Line-Signature'] # get request body as text body = request.get_data(as_text=True) app.logger.info("Request body: " + body) # handle webhook body try: handler.handle(body, signature) except InvalidSignatureError: abort(400) @handler.add(MessageEvent, message=TextMessage) def handle_message(event): """ UsersのActivityによって条件分岐。 noneQuestionがデフォルト→メニューを表示。 waitQestionが質問待ち状態→次に入力されたテキストが質問になる。 """ UserID = event.source.user_id text = event.message.text activity = users_DB.checkActivity(UserID) if activity == "noneQuestion": line_bot_api.reply_message( event.reply_token, TextSendMessage( text='メニュー', quick_reply=QuickReply( items=[ QuickReplyButton( action=PostbackAction(label="異性に質問してみる", data="異性に質問してみる") ), QuickReplyButton( action=PostbackAction(label="誰かの質問に答える", data="誰かの質問に答える") ), ]))) elif activity == "waitQestion": sex = users_DB.getSex(UserID) questions.add(UserID,sex,text) users_DB.changeActivity(UserID,"waitAnswer") line_bot_api.reply_message( event.reply_token, TextSendMessage( text=text + "があなたの質問として投稿されました!", quick_reply=QuickReply( items=[ QuickReplyButton( action=PostbackAction(label="自分への回答を確認する", data="回答を確認する") ), QuickReplyButton( action=PostbackAction(label="誰かの質問に答える", data="誰かの質問に答える") ), QuickReplyButton( action=PostbackAction(label="質問を変更する", data="異性に質問してみる") ), ]))) elif activity == "waitAnswer": line_bot_api.reply_message( event.reply_token, TextSendMessage( text='メニュー', quick_reply=QuickReply( items=[ QuickReplyButton( action=PostbackAction(label="自分への回答を確認する", data="回答を確認する") ), QuickReplyButton( action=PostbackAction(label="誰かの質問に答える", data="誰かの質問に答える") ), QuickReplyButton( action=PostbackAction(label="質問をする", data="異性に質問してみる") ), ]))) else: QuestionKey = activity questions.putAnswer(UserID,text,QuestionKey) users_DB.changeActivity(UserID,"waitAnswer") line_bot_api.reply_message( event.reply_token, TextSendMessage( text=text + "をあなたの回答として投稿しました", quick_reply=QuickReply( items=[ QuickReplyButton( action=PostbackAction(label="自分への回答を確認する", data="回答を確認する") ), QuickReplyButton( action=PostbackAction(label="誰かの質問に答える", data="誰かの質問に答える") ), QuickReplyButton( action=PostbackAction(label="質問をする", data="異性に質問してみる") ), ]))) return "ok" @handler.add(FollowEvent) def handle_follow(event): """ 友だち追加したときのイベント。 UsersDBにIDと性別、アクティビティを追加。 """ UserID = event.source.user_id users_DB.add(UserID) buttons_template = ButtonsTemplate( title='友達追加ありがとう!', text='まず、あなたの性別を教えてください!', actions=[ PostbackAction(label='男', data='male'), PostbackAction(label='女', data='female'), ]) template_message = TemplateSendMessage(alt_text='友達追加ありがとう!\nまず、あなたの性別を教えてください。', template=buttons_template) line_bot_api.reply_message(event.reply_token, template_message) @handler.add(UnfollowEvent) def handle_unfollow(event): """ ブロックされた時のイベント。 UsersDBのIDと性別、アクティビティを削除。 <TODO> QuestionDBの投稿を削除。 """ UserID = event.source.user_id users_DB.remove(UserID) @handler.add(PostbackEvent) def handle_postback(event): """ ポストバックに対応したメソッド。 性別の登録。 質問の投稿。 回答の投稿。 """ UserID = event.source.user_id if event.postback.data == 'male': sex = "male" users_DB.updateSex(UserID,sex) line_bot_api.reply_message( event.reply_token, TextSendMessage( text='男性として登録されました!さっそく質問をしてみましょう!', quick_reply=QuickReply( items=[ QuickReplyButton( action=PostbackAction(label="異性に質問してみる", data="異性に質問してみる") ), QuickReplyButton( action=PostbackAction(label="誰かの質問に答える", data="誰かの質問に答える") ), ]))) elif event.postback.data == 'female': sex = "female" users_DB.updateSex(UserID,sex) line_bot_api.reply_message( event.reply_token, TextSendMessage( text='女性として登録されました!さっそく質問をしてみましょう!', quick_reply=QuickReply( items=[ QuickReplyButton( action=PostbackAction(label="異性に質問してみる", data="異性に質問してみる") ), QuickReplyButton( action=PostbackAction(label="誰かの質問に答える", data="誰かの質問に答える") ), ]))) elif event.postback.data == "異性に質問してみる": users_DB.changeActivity(UserID,"waitQestion") line_bot_api.reply_message( event.reply_token, TextSendMessage(text="質問を入力してね!")) elif event.postback.data == "誰かの質問に答える": q = questions.getOtherQuestion(UserID) if q == False: line_bot_api.reply_message( event.reply_token, TextSendMessage( text='未回答の異性の質問はありませんでした', quick_reply=QuickReply( items=[ QuickReplyButton( action=PostbackAction(label="自分への回答を確認する", data="回答を確認する") ), QuickReplyButton( action=PostbackAction(label="誰かの質問に答える", data="誰かの質問に答える") ), QuickReplyButton( action=PostbackAction(label="質問をする", data="異性に質問してみる") ), ]))) else: users_DB.changeActivity(UserID,q[1]) line_bot_api.reply_message( event.reply_token, TextSendMessage(text="質問:" + q[0] +"\n\nこの質問に対する回答を入力してください")) elif event.postback.data == "回答を確認する": key = users_DB.getQuestionKey(UserID) a = questions.getAnserForMe(key) answer = a[0] question = a[1] if answer == "": line_bot_api.reply_message( event.reply_token, TextSendMessage( text='まだあなたの質問への回答はありません。\nもう少し時間がたってからまた試してみてね!', quick_reply=QuickReply( items=[ QuickReplyButton( action=PostbackAction(label="自分への回答を確認する", data="回答を確認する") ), QuickReplyButton( action=PostbackAction(label="誰かの質問に答える", data="誰かの質問に答える") ), QuickReplyButton( action=PostbackAction(label="質問をする", data="異性に質問してみる") ), ]))) else: line_bot_api.reply_message( event.reply_token, TextSendMessage( text=question + 'に対する回答が届いています!\n\n「' + answer + "」", quick_reply=QuickReply( items=[ QuickReplyButton( action=PostbackAction(label="次の質問をする", data="異性に質問してみる") ), QuickReplyButton( action=PostbackAction(label="誰かの質問に答える", data="誰かの質問に答える") ), ]))) if __name__ == "__main__": app.run()
解説するほどのコードではないですが、簡単にまとめておきます。
import users_DB import questions
これは独自に定義したソースファイルです。
users_DBがスプレッドシートにユーザーの情報を保存する機能を持っています。
questionsは、ユーザーの投稿した質問と、それに対する回答を扱っています。
また、メニュー代わりとして、LINEのクイックリプライ機能を使っています。
本当はリッチメニューを使いたかったのですが、フリープランでは残念ながら使えなかったので…。
ユーザーの操作はpostbackイベントで区別させています。
また、DBにユーザーの状態を表す項目を追加し、それを変化させることによって、質問の投稿や回答の投稿などの操作を区別しています。
随分雑な説明ではありますが、ご容赦を。
ユーザの情報を保存する
今回は、ユーザーの情報を保存するDBとして、スプレッドシートを用いています。
設定したフィールドは、
・id
・UserId(これでプッシュメッセージを送れる)
・性別
・アクティビティ(質問の入力待ちなどの状態を判別する項目)
の4つ。
コードはこちら。
from oauth2client.service_account import ServiceAccountCredentials from httplib2 import Http import gspread scopes = ['https://www.googleapis.com/auth/spreadsheets'] json_file = 'OAuth用クライアントIDの作成でダウンロードしたjsonファイル' credentials = ServiceAccountCredentials.from_json_keyfile_name(json_file, scopes=scopes) http_auth = credentials.authorize(Http()) # スプレッドシート用クライアントの準備 doc_id = 'ドキュメントのID client = gspread.authorize(credentials) gfile = client.open_by_key(doc_id) worksheet = gfile.worksheet('シート名') def add(userID): key = worksheet.cell(1,1).value key = int(key) worksheet.update_cell(key+1, 1, str(key)) worksheet.update_cell(key+1, 2, userID) worksheet.update_cell(key+1, 4, "noneQuestion") worksheet.update_cell(1, 1, str(key+1)) return "ok" def remove(UserID): cell = worksheet.find(UserID) row = cell.row worksheet.delete_row(row) return "ok" def updateSex(UserID,sex): cell = worksheet.find(UserID) worksheet.update_cell(cell.row, cell.col+1, sex) def changeActivity(UserID,activity): cell = worksheet.find(UserID) worksheet.update_cell(cell.row, cell.col+2, activity) def checkActivity(UserID): cell = worksheet.find(UserID) activity = worksheet.cell(cell.row, cell.col+2).value return activity def getSex(UserID): cell = worksheet.find(UserID) sex = worksheet.cell(cell.row, cell.col+1).value return sex def getQuestionKey(UserID): cell = worksheet.find(UserID) answerKey = worksheet.cell(cell.row, cell.col+3).value return answerKey def changeQuestionID(UserID,Qkey): cell = worksheet.find(UserID) worksheet.update_cell(cell.row, cell.col+3, Qkey) return "ok"
今回、UserIDで特定したデータの行の中から特定のフィールドを抜き出す時、列番号を足し引きして指定するという、保守性のかけらもない方法をとってしまいました。
割と後悔するのが心底恥ずかしいコードです。
本当は列の頭にあるフィールド名で指定したかったんだけど、実装方法がわかりませんでした。
やっぱりちゃんとDB使えるようになりたいな。
質問文を格納し、取り出す
もはや語るべきものはなく。
コードだけ貼っておきます。
from oauth2client.service_account import ServiceAccountCredentials from httplib2 import Http import gspread import users_DB scopes = ['https://www.googleapis.com/auth/spreadsheets'] json_file = 'OAuth用クライアントIDの作成でダウンロードしたjsonファイル' credentials = ServiceAccountCredentials.from_json_keyfile_name(json_file, scopes=scopes) http_auth = credentials.authorize(Http()) # スプレッドシート用クライアントの準備 doc_id = 'ドキュメントのID' client = gspread.authorize(credentials) gfile = client.open_by_key(doc_id) #読み書きするgoogle spreadsheet worksheet = gfile.worksheet('シート名') def add(UserID,Sex,Question): key = worksheet.cell(1,1).value key = int(key) worksheet.update_cell(key+1, 1, str(key)) worksheet.update_cell(key+1, 2, Sex) worksheet.update_cell(key+1, 3, UserID) worksheet.update_cell(key+1, 4, Question) worksheet.update_cell(1, 1, str(key+1)) users_DB.changeQuestionID(UserID,key) return "ok" def remove(QuestionID): cell = worksheet.find(QuestionID) row = cell.row worksheet.deleate_row(row) return "ok" def getOtherQuestion(UserID): sex = users_DB.getSex(UserID) maxQuestionKey = int(worksheet.cell(1,1).value) if sex == "male": keysex = "female" else: keysex = "male" try: cells = worksheet.findall(keysex) for cell in cells: if worksheet.cell(cell.row, cell.col + 4).value == "": q = [] Question = worksheet.cell(cell.row,cell.col + 2).value key = worksheet.cell(cell.row,cell.col - 1).value q.append(Question) q.append(key) return q return False。 except: return False def putAnswer(UserID,Answer,QuestionID): cell = worksheet.find(QuestionID) worksheet.update_cell(cell.row, cell.col + 4, Answer) worksheet.update_cell(cell.row, cell.col + 5, UserID) def getAnserForMe(key): cell = worksheet.find(key) a = [] question = worksheet.cell(cell.row, cell.col + 3).value answer = worksheet.cell(cell.row, cell.col + 4).value a.append(answer) a.append(question) return a
まとめ
今回は単純に作ったものを記録しておくための記事なので、説明を省きまくって雑に書いてしまいました。
お恥ずかしい限りです。