# LINEのチャットbotを作る3 ### <font color='red'>使用したもの</font> --- * Windows10 * テキストエディタ 秀丸でPython書いてました:sweat_smile: * heroku * flask * GitCMD * Googlespreadsheet DBの扱いがよくわからなかったのでDBの代用品 ### <font color='red'>チャットbotの概要</font> --- 今回作ったチャットbotは, クロネコヤマト公式アカウントのように事前に用意したプログラムが, クラブチームに所属する子供たちの欠席•遅刻の連絡対応から監督への通知まで自動化するものです. 保護者の方は用意されたアカウントと会話するだけで連絡でき, 監督も一箇所を見れば連絡を確認できます. ただ完全CUIです. すごく不便です. 動きとしては下の画像そのままです. ![](https://i.imgur.com/UoqVjqM.png) ### <font color='red'>コード解説</font> --- てなわけで解説書いていきます. 何部構成になるかわからないけど, 気ままに書いていけたらなと. インデント少しずれてるとこあるけど, 合わせたらこうなってしまったんだもん. 同じことしてるのに合うとこ合わないとこあるのか俺が一番聞きたいよ. ```python= > BadMain.py 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, UnfollowEvent, FollowEvent ) import os import json import gspread import oauth2client import linebot import BadPush #アクセスキーの取得 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("/") def hello_world(): return "hello world!" @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(FollowEvent) def handle_follow(event): """ 友だち追加したときのイベント. UsersDBにID, アクティビティを追加. """ UserID = event.source.user_id BadPush.add(UserID) line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text='minatoJBSC【公式】です. \n友達追加ありがとうございます!!'), TextSendMessage(text='これからは練習の休み, 遅刻の連絡はこのアカウントからお願いします. '), TextSendMessage(text='本日の練習をお休みする場合は「休み」, 遅れて参加の場合は「遅刻」と送信して下さい. ') ] ) @handler.add(UnfollowEvent) def handle_unfollow(event): """ ブロックされた時のイベント. UsersDBのIDとアクティビティを削除. """ UserID = event.source.user_id BadPush.remove(UserID) @handler.add(MessageEvent, message=TextMessage) def handle_message(event): """ UsersのActivityによって条件分岐. noneQuestionがデフォルト→メニューを表示. waitQestionが質問待ち状態→次に入力されたテキストが質問になる. """ UserID = event.source.user_id text = event.message.text activity = BadPush.checkActivity(UserID) if activity == "初期状態": if (text =="休み")or(text =="休")or(text =="やすみ"): BadPush.changeActivity(UserID,"休み遅刻") BadPush.changeStatus(UserID,"休み") line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text='お休みですね. \n理由を選択して送信して下さい. \n「家庭都合」, 「体調不良」, 「怪我」, 「その他」') ] ) elif (text =="遅刻")or(text =="遅")or(text =="ちこく"): BadPush.changeActivity(UserID,"休み遅刻") BadPush.changeStatus(UserID,"遅刻") line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text='遅刻ですね. \n到着予定時間(見込)を送信して下さい. ') ] ) else: line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text='申し訳ありませんその言葉は理解しかねます. \n「休み」もしくは「遅刻」と送信して下さい. ') ] ) elif activity == "休み遅刻": ChildStat = BadPush.getStatus(UserID) if (ChildStat =="休み"): if (text =="家庭都合")or(text =="体調不良")or(text =="怪我")or(text =="その他"): BadPush.changeActivity(UserID,"選手名") BadPush.changeReason(UserID,text) line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text='了解しました. \nお子さんの名前をフルネームで送信して下さい. ') ] ) else: line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text='お休みの理由を選択して送信して下さい. \n「家庭都合」, 「体調不良」, 「怪我」, 「その他」') ] ) elif (ChildStat =="遅刻"): BadPush.changeActivity(UserID,"選手名") BadPush.changeReason(UserID,text) line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text='了解しました. \nお子さんの名前をフルネームで送信して下さい. ') ] ) elif activity == "選手名": BadPush.changePlayer(UserID,text) BadPush.changeActivity(UserID,"補足") line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text='その他補足事項等があれば入力し送信して下さい. \n特になければ「なし」で送信して下さい. ') ] ) elif activity == "補足": BadPush.changeRemarks(UserID,text) BadPush.changeActivity(UserID,"最終状態") Status_text = BadPush.getStatus(UserID) Reason_text = BadPush.getReason(UserID) Player_text = BadPush.getPlayer(UserID) Remarks_text = BadPush.getRemarks(UserID) line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text=Player_text+"\n"+Status_text+":"+Reason_text+"\n"+Remarks_text), TextSendMessage(text="上記で登録します. よろしければ「はい」を, 訂正がある場合は「いいえ」を送信して下さい. ") ] ) elif activity =="最終状態": if ( text == 'はい'): BadPush.changeActivity(UserID,"初期状態") Status_text = BadPush.getStatus(UserID) Reason_text = BadPush.getReason(UserID) Player_text = BadPush.getPlayer(UserID) Remarks_text = BadPush.getRemarks(UserID) to = ["通知飛ばしたい人のIDを書く(複数人に送りたかったら,でつなぐ)"] messages = TextSendMessage(text=Player_text+"\n"+Status_text+":"+Reason_text+"\n"+Remarks_text) line_bot_api.multicast(to, messages=messages) line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text = '連絡を受け付けました. ありがとうございました. '), TextSendMessage(text='本日の練習をお休みする場合は「休み」, 遅れて参加の場合は「遅刻」と送信して下さい. ') ] ) elif (text == 'いいえ'): BadPush.changeActivity(UserID,"初期状態") line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text = 'お手数ですが初めからやり直して下さい. '), TextSendMessage(text='本日の練習をお休みする場合は「休み」, 遅れて参加の場合は「遅刻」と送信して下さい. ') ] ) else: Status_text = BadPush.getStatus(UserID) Reason_text = BadPush.getReason(UserID) Player_text = BadPush.getPlayer(UserID) Remarks_text = BadPush.getRemarks(UserID) line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text=Player_text+"\n"+Status_text+":"+Reason_text+"\n"+Remarks_text), TextSendMessage(text="上記で登録します. よろしければ「はい」を, 訂正がある場合は「いいえ」を送信して下さい. ") ] ) if __name__ == "__main__": port = int(os.getenv("PORT",5000)) app.run(host="0.0.0.0", port=port) ``` ```python= > Badpush.py from oauth2client.service_account import ServiceAccountCredentials import gspread import json import random scopes = [''https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive'] json_file = '秘密鍵のJSON' credentials = ServiceAccountCredentials.from_json_keyfile_name(json_file, scopes=scopes) # スプレッドシート用クライアントの準備 client = gspread.authorize(credentials) SPREADSHEET_KEY = 'スペレッドーシートの情報' worksheet = client.open_by_key(SPREADSHEET_KEY).sheet1 def add(userID): num = worksheet.cell(1,1).value num = int(num) for i in range(10): # 最大10回実行 try: worksheet.update_cell(num+1, 1, str(num)) worksheet.update_cell(num+1, 2, userID) worksheet.update_cell(num+1, 3, "初期状態") worksheet.update_cell(num+1, 4, "休み/遅刻") worksheet.update_cell(num+1, 5, "理由/時間") worksheet.update_cell(num+1, 6, "選手名") worksheet.update_cell(num+1, 7, "備考") worksheet.update_cell(1, 1, str(num+1)) except Exception as e: # 例外発生時は0~4秒の時間待ち. 同期ずらし. ran = random.randrange(0,4) sleep(ran) else: # 例外が発生しなかった場合, for を break break def remove(UserID): num = worksheet.cell(1,1).value num = int(num) cell = worksheet.find(UserID) row = cell.row if row != 0: for i in range(10): # 最大10回実行 try: worksheet.delete_row(row) except Exception as e: # 例外発生時は0~4秒の時間待ち. 同期ずらし. ran = random.randrange(0,4) sleep(ran) else: # 例外が発生しなかった場合, for を break break def checkActivity(UserID): cell = worksheet.find(UserID) activity = worksheet.cell(cell.row, 3).value return activity def changeActivity(UserID,activity): cell = worksheet.find(UserID) for _ in range(10): # 最大10回実行 try: worksheet.update_cell(cell.row, 3, activity) except Exception as e: # 例外発生時は0~4秒の時間待ち. 同期ずらし. ran = random.randrange(0,4) sleep(ran) else: # 例外が発生しなかった場合, for を break break def changeStatus(UserID,ChildStatus): cell = worksheet.find(UserID) for _ in range(10): # 最大10回実行 try: worksheet.update_cell(cell.row, 4, ChildStatus) except Exception as e: # 例外発生時は0~4秒の時間待ち. 同期ずらし. ran = random.randrange(0,4) sleep(ran) else: # 例外が発生しなかった場合, for を break break def getStatus(UserID): cell = worksheet.find(UserID) ChildStatus = worksheet.cell(cell.row, 4).value return ChildStatus def changeReason(UserID,Reason): cell = worksheet.find(UserID) for i in range(10): # 最大10回実行 try: worksheet.update_cell(cell.row, 5, Reason) except Exception as e: # 例外発生時は0~4秒の時間待ち. 同期ずらし. ran = random.randrange(0,4) sleep(ran) else: # 例外が発生しなかった場合, for を break break def getReason(UserID): cell = worksheet.find(UserID) Reason = worksheet.cell(cell.row, 5).value return Reason def changePlayer(UserID,PlayerName): cell = worksheet.find(UserID) for _ in range(10): # 最大10回実行 try: worksheet.update_cell(cell.row, 6, PlayerName) except Exception as e: # 例外発生時は0~4秒の時間待ち. 同期ずらし. ran = random.randrange(0,4) sleep(ran) else: # 例外が発生しなかった場合, for を break break def getPlayer(UserID): cell = worksheet.find(UserID) PlayerName = worksheet.cell(cell.row, 6).value return PlayerName def changeRemarks(UserID,Remarks): cell = worksheet.find(UserID) for i in range(10): # 最大10回実行 try: worksheet.update_cell(cell.row, 7, Remarks) except Exception as e: # 例外発生時は0~4秒の時間待ち. 同期ずらし. ran = random.randrange(0,4) sleep(ran) else: # 例外が発生しなかった場合, for を break break def getRemarks(UserID): cell = worksheet.find(UserID) Remarks = worksheet.cell(cell.row, 7).value return Remarks ``` この2つのプログラム使って運用してます. BadMain.pyは処理, BadPush.pyは関数書いて呼ばれてって感じです. 早速解説書いていきます.2つのプログラムを必要に応じて同時に解説し出したりするので, 先頭行見てどっちのプログラムについてなのか判断してください. なんか分かりやすい書き方ないかなとは思ってますよ. けどこのまま行かせてくださいな. てかなんでBadpush.py色ついてくれんの. ```python= > BadMain.py 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("/") def hello_world(): return "hello world!" @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) ``` この辺は https://hackmd.io/D8vnukwkQHCh3w6ntSlQVg ここに書いてあることと一緒なので説明を割愛. 友達追加の部分の解説からいきます. textのメッセージを変えることで, botアカウントを友達追加した時に送信するメッセージを好きに設定できます. また, TextSendMessage(text)で1メッセージだから, 今回の場合は友達追加したら3メッセージに分けられて登録者へと送信されます. ```python= >BadMain.py @handler.add(FollowEvent) def handle_follow(event): """ 友だち追加したときのイベント. UsersDBにID, アクティビティを追加. """ UserID = event.source.user_id BadPush.add(UserID) line_bot_api.reply_message(event.reply_token, [ TextSendMessage(text='minatoJBSC【公式】です. \n友達追加ありがとうございます!!'), TextSendMessage(text='これからは練習の休み, 遅刻の連絡はこのアカウントからお願いします. '), TextSendMessage(text='本日の練習をお休みする場合は「休み」, 遅れて参加の場合は「遅刻」と送信して下さい. ') ] ) ``` 友達追加された時の処理を見ていきます. ```python= > BadPush.py def add(userID): num = worksheet.cell(1,1).value num = int(num) for i in range(10): # 最大10回実行 try: worksheet.update_cell(num+1, 1, str(num)) worksheet.update_cell(num+1, 2, userID) worksheet.update_cell(num+1, 3, "初期状態") worksheet.update_cell(num+1, 4, "休み/遅刻") worksheet.update_cell(num+1, 5, "理由/時間") worksheet.update_cell(num+1, 6, "選手名") worksheet.update_cell(num+1, 7, "備考") worksheet.update_cell(1, 1, str(num+1)) except Exception as e: # 例外発生時は0~4秒の時間待ち. 同期ずらし. ran = random.randrange(0,4) sleep(ran) else: # 例外が発生しなかった場合, for を break break ``` ![](https://i.imgur.com/dWOsSwN.png) 上の画像みたいに行単位で情報管理してます.spreadsheetの(1,1)には初め1が格納されています. なぜそれが必要なのか, 行番号の拾い方が分からなかったから無理矢理自分で探してるからです. だから1行目は(1,1)以外は空白になっていて, 2行目以降からユーザーの情報を保管しています. 実際に友達追加が行われた時, (1,1)の値を見て, その値+1行目にユーザーの情報を格納します. 画像で説明すると, 番号の箇所には(1,1)の値, ユーザーIDには個別のID, 状態は今何の項目について入力されているのかを識別するものが入っていきます. 9〜13行目は順を追って説明していくので今は, ここにはこんな情報が入るんだな程度でOKです. 大事なのは14行目. (1,1)の値を+1してあげます. これをしないと, spreadsheetのどこが空いているか見失ってしまうからです. またfor文でtry, exceptしているのは友達追加のタイミングが万一被った時用なので保険です. 次は友達から削除された時のこと. ほとんど友達追加の時と同じ動きで逆やってるだけです. ブロックされた時行ごと全て消しています. ```python= > BadMain.py @handler.add(UnfollowEvent) def handle_unfollow(event): """ ブロックされた時のイベント. UsersDBのIDとアクティビティを削除. """ UserID = event.source.user_id BadPush.remove(UserID) ``` ```python= > BadPush.py def remove(UserID): num = worksheet.cell(1,1).value num = int(num) cell = worksheet.find(UserID) row = cell.row if row != 0: for i in range(10): # 最大10回実行 try: worksheet.delete_row(row) except Exception as e: # 例外発生時は0~4秒の時間待ち. 同期ずらし. ran = random.randrange(0,4) sleep(ran) else: # 例外が発生しなかった場合, for を break break ``` ここで案外重要なのが, さっきは(1,1)の値を+1してたけど今回は-1してません. -1して成り立つのは最後の行に登録されている人だけで, 途中の行の人からブロックされた時にエラーが起きてしまいます. 具体的には, もう記入済みの最後の行に次登録した人の情報を登録しようとしてしまうからです. コードとか貼ってたら長くなってしまったので, 4に託そうかなと思います. 頑張れよ自分.