LINE公式垢でボットとチャット両立させてみた！

# はじめに

こんにちは、[42tokyo Advent Calendar 2021](https://qiita.com/advent-calendar/2021/42tokyo)の24日目を担当する、42tokyoの在校生のtharaです。今回は、私が個人的に行っているTeenMakersというプロジェクトのメールフォームを撤廃し、公式LINEアカウントを導入するお話です。

TeenMakersは「中高生が社会人に取材をする」サービスで、42の方にも数名お世話になっています。大変ありがとうございます。TeenMakersのウェブサイトはこちら！

https://teenmakers.jp/

LINE公式垢にはBotモードとチャットモードがあります。Botモードでは、Webhookなどが利用でき、イベントに対する処理のカスタマイズがかなり出来ます。チャットモードでは手動での返信 + 簡単な自動応答ができるみたいです。そして、現状その両立が出来ないという。。。

![linebot_setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1212145/c50089cb-fc47-e7a6-be23-99ca9d0739b0.png)

しかし、僕が42でいつもお世話になっているkkonishiさんがSlack APIを活用し、Webhookを用いたイベント処理と手動でのチャット対応を両立させてくれました！（お前じゃないんかいっていうツッコミは無しでお願いします笑）


完成品はこちら。（42でお世話になっている二人の方をNoにするという最悪のデモです。時間が無かったんです。許してください笑）

|　　　　　　　　  　　　　　　　　   　　　　　　　　　　　　　　　　　　　　　　　　　|
|:-:|
|![demo1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1212145/a336d711-d9db-128e-6e16-33d59a8dd5e4.gif)|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1212145/194c9814-1127-682b-3d5c-49f2bd9dc9bf.gif" width="800" >|



レポジトリーはこちら

https://github.com/tacomeet/linebot-tm/tree/41a5b0bcef922a9c9d0a9f1c52ec8bbbd8ac2b2d


LINEアカウントはこちら
<a href="https://lin.ee/BbMdvBt"><img src="https://scdn.line-apps.com/n/line_add_friends/btn/ja.png" alt="友だち追加" height="36" border="0"></a>


# Botモードのオススメ機能と記事作成機能

今回Botモードでは2つの機能を実装しました！

1. 記事作成機能
誰かと話した時にそれの振り返りをしたい思ったことはないですか？これは、その振り返りをLINEボットと会話をすることで簡単にできる機能です。

2. 社会人おすすめ機能
TeenMakersには現在20人ほどの社会人にご登録頂いており、中高生はそのプロフィールを見て取材を申し込むという形になっています。今後もっとプロジェクトが大きくなるかもしれません！その時に、中高生が社会人のプロフィールを全て見て自分にマッチした人を探すのは大変なのではないか？ということで、いくつかの質問に答えるだけでオススメの社会人を教えてくれる機能です！

# どうやって会話するの...？

LINEボットの簡単な仕組みとしては↓です。

> 友だち追加やメッセージの送信のようなイベントが発生すると、LINEプラットフォームからWebhook URL（ボットサーバー）にHTTPS POSTリクエストが送信されます。 (**[Messaging APIリファレンス](https://developers.line.biz/ja/reference/messaging-api/)より**)
> 

このHTTPS POSTリクエストの種類に応じて、メッセージの返信やその他のアクションを行うサーバーを用意する形です。単体のメッセージに関して特定の返信をすることは簡単ですが、実際にやりたいことはこんな感じのフローチャートでした。

![linebot_bn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1212145/9db41f10-407b-85a2-e5da-c8298314297f.png)


今回は、[こちら](https://toukei-lab.com/recommend-algorithm)の投稿を参考にさせて頂き、ユーザー毎にどの種類の会話なのか、またどのステージまで行っているのか保存することで会話を実現しました！ソースコードに関しても上記の投稿のものを少し変えて使わせて頂きました！

```python
# status.py
class Status:
    def __init__(self):
        # 何番めの会話まで行ったか
        self.context = 0
        # 会話の種類
        self.type = None
		
# 会話の種類
class Type(Enum):
    BN_CREATE = auto()
    BN_CREATE_TRACK1 = auto()
    BN_CREATE_TRACK2 = auto()
    BN_CREATE_TRACK3 = auto()
    BN_CREATE_TRACK5 = auto()
    SELF_REF = auto()
    CATCH_REC = auto()
    CONTACT = auto()
```

```python
# session.py
class Session:
    # ユーザーIDとStatusの辞書
    _status_map = dict()
```

こちらを用いることで、1の「記事作成機能」に関しては完成出来ました。

# 推薦アルゴリズム

[こちら](https://toukei-lab.com/recommend-algorithm)の記事によると推薦アルゴリズムにも色々な種類あるようです。協調フィルタリングなんてすごいワクワクしますが、今回は制約が甘いのもあり、単純に全探索です。別の良い方法をご存知の方がいらっしゃったら、是非教えてください！

1. 事前に社会人の候補を全て持っておきます。TeenMakersのサイトはWordPressで作られているのですが、自動で[REST API](https://teenmakers.jp/wp-json/wp/v2/posts)が出来ているので、そちらから取得する形にしました。
2. 各社会人毎にその人の職種や経験などから`Tag`と言われるものがついています。事前にそのTagに基づいた質問を用意しておき、その質問への’Yes’ ‘No’の回答で選択肢を絞っていきます。
3. 社会人の方で付いている全てのタグが’Yes’になった方がいれば、その方を推薦します。
4. これにも’Yes’ ‘No’で回答できるようにし、’Yes’であれば終了。’No’であれば、2に戻って質問を続けていきます。

# Botモードで手動チャット機能を実装

今回、手動チャットで行いのは、マニュアルでのお問い合わせ対応です。

以下kkonishiさんの寄稿文です。（笑）


LINEボットに送られてきたお問い合わせの内容をSlackのスレッドを使って管理したら楽なのでは？と考え、実装してみました。


まず、[こちらの記事](https://zenn.dev/takaki/articles/slack-python-api-message)を参考にSlack API周り（Slack Botに対してのアクセス権限やトークンの取得など）、PythonのSlack SDKの導入など、SlackとPythonを連携させる準備を行いました。

この準備ができた時点で、Python → Slackにメッセージを送る処理はほぼできました。

次に、Slack → Pythonにメッセージを送る処理を[こちらの記事](https://qiita.com/stu345/items/030dbfca8363583d24ad) を参考に実装しました。

お問い合わせがあった時点でスレッドを作成し、運営側からの返信やユーザーからのお問い合わせ内容の送信などを一つのスレッドで管理できるようにしました。

簡単に処理の手順を説明します。

1. お問い合わせしたユーザーとSlackのスレッドを紐づけるために`Contact`クラスを用意する
    
    ```python
    # contact.py
    class Contact:
    	def __init__(self) -> None:
    		self._thread_map = dict()
    		self._user_id_map = dict()
    
    	def register(self, user_id, thread_ts):
    		self._set_thread(user_id, thread_ts)
    		self._set_user(user_id, thread_ts)
    
    	def _set_thread(self, user_id, thread_ts):
    		self._thread_map[user_id] = thread_ts
    
    	def _set_user(self, user_id, thread_ts):
    		self._user_id_map[thread_ts] = user_id
    
    	def get_thread(self, user_id):
    		return self._thread_map.get(user_id)
    
    	def get_user(self, thread_ts):
    		return self._user_id_map.get(thread_ts)
    ```
    
2. ユーザーが「お問い合わせ」と送信した際に、Slack Botが「 `user` さんからのお問い合わせがありました！」というメッセージをSlackのチャンネルに投稿する。同時に、このお問い合わせをスレッドとして作成するために、メッセージのthread_idとuser_idを紐づけておく。
    
    ```python
    # app.py
    con = contact.Contact()
    
    profile = line_bot_api.get_profile(user_id)
    res = slack.start_contact(profile.display_name)
    con.register(user_id, res['message']['ts'])
    ```
    
3. お問い合わせの具体的な内容を`user`が送信すると、手順1で作成したスレッドの返信にそのメッセージが送られる。
    
    ```python
    # app.py
    profile = line_bot_api.get_profile(user_id)
    slack.send_msg_to_thread(profile.display_name, text, con.get_thread(user_id))
    ```
    
4. 手順2で送られてきたスレッドの内容に対して、Slackのお問い合わせチャンネルに参加している運営が（チャンネルに参加している人なら誰でも可）返信する。
5. 手順3で運営が返信した内容がLINEbotに送られ、ユーザー側に表示される。
    
    ```python
    # app.py
    # Slackからメッセージが送られてくるイベントをキャッチする
    @app.route('/', methods=["POST"])
    def index():
        data = request.data.decode('utf-8')
        data = json.loads(data)
        # for challenge of slack api
        if 'challenge' in data:
            token = str(data['challenge'])
            return Response(token, mimetype='text/plane')
        # for events which you added
        if 'event' in data:
            event = data['event']
            app.logger.info(event)
            reply_contact(event)
        return 'OK'
    
    # LINEbotにSlackのメッセージを送る
    def reply_contact(event):
        if 'bot_id' not in event:
            thread_ts = event['thread_ts']
            user_id = con.get_user(thread_ts)
            msg = event['text']
            line_bot_api.push_message(user_id, TextSendMessage(text=msg))
    ```
    
6. ユーザーが「終了」と入力するとお問い合わせが終了する。

細かい実装についてはリポジトリを覗くか、僕に連絡ください。

![linebot_slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1212145/464ef87f-07d6-913a-97d0-47b401a9e188.png)


LINE側からは、運営によるチャット対応は全てLINE botがやっているように見え、Slack側からは、ユーザーからのお問い合わせは全てSlack botがやっているように見える、っていう感じですね。

このように、LINEbot, Slack API, Pythonを組み合わせることによって、お問い合わせに対してはチャット対応、他の機能に関しては自動応答みたいに、LINEbotの2つのモードを切り替えずに両立することができました。（厳密に両立というには、ちょっと甘いかもですが。。。）

現実的に、担当者１人が解決できるケースは少ないと思いますし、Slackのスレッドにお問い合わせの履歴が残っていれば、似たようなお問い合わせが来た時にもすぐ過去のやりとりを引っ張ってこれたりできて便利かなぁ〜と思います。

# LINEボットを自分で作ってみたい方向け

自分達が参考にしたサイトなどを読んで行った順にご紹介いたします。

1. [【入門用】PythonによるLINEbot作り方](https://www.tomotaku.com/line-repeat-bot/)
まずはここに書いてあることを全て順番に行いました。LINEボットのイメージが掴めます。

2. [flask-kitchensink Repository](https://github.com/line/line-bot-sdk-python/tree/master/examples/flask-kitchensink)
LINE公式の実装例です。kitchensinkに「出来得る限りすべてのもの」という意味があるらしく、「こんなことも出来るんだー！」と思いながら、気になるところを写経していきました。

3. [PythonでLINE BOT開発で２ターン以上の会話の作り方がわからない。](https://teratail.com/questions/331825)
Pythonの実装例まであり、大変ありがたかったです。

4. [Slack APIをPython SDKで使う](https://zenn.dev/takaki/articles/slack-python-api-message)
SlackとPythonを連携させるための手順から実装例まで丁寧に解説してくださってます。

5. [SlackからPythonサーバーにメッセージを送信する](https://qiita.com/stu345/items/030dbfca8363583d24ad)
Slack→Pythonにメッセージを送るためのイベントフックの設定などわかりやすく解説してくださってます。

6. [[LINEBOT] Herokuへデプロイ ③](https://zenn.dev/shin_shin_01/articles/98f81192522356)
deploy自体はこの記事を参考にさせて頂きました。

# まとめ

LINEボットはローカルから動かすのがDiscordボットなどに比べて少し面倒くさかったですが、普段みんなが慣れ親しんだアプリのUIを使えるで、プログラミング学習の教材として割と良いものかもしれません。

42では年中入学者を募集しています！気になる方はこちらをチェック！

https://42tokyo.jp/

明日は、hiroinさんが「tracerouteの出力を42生が調べたら」について書いてくれる予定ですので、そちらの記事もお楽しみに！