Pythonを使ってLine-botを作ってみた！

# 見つけた課題

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363957/9ddca733-954c-8e4b-b9fb-9e85360c0c31.png)

#目的と機能
好きなお店・施設を好きな名前で登録して、そのお店・施設の情報を取ってこれるLine-botを作る。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363957/9cedb46e-a12b-4cdd-f262-fc2cd4ea6ea5.png)

#必要なもの
- Python
- line-api
- git
- heroku
- google-api

# 開発の流れ
## 1 [LINE Developers](https://developers.line.biz/ja/)と[heroku](https://dashboard.heroku.com/apps)に登録（省略）
## 2 LINEが配っている[サンプルコード](https://github.com/line/line-bot-sdk-python)でとりあえずLINE-botを作ってみる。

### app.py
Lineが配っているサンプルコード。

```python


from flask import Flask, request, abort

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage,
)

app = Flask(__name__)

# line-developersのページからaccess_tokenとchannel_secretをそれぞれ生成してここで変数に入れる
# 分からない場合はほかの記事参照。他の人がたくさん書いてるのでここでは省略。
line_bot_api = LineBotApi('YOUR_CHANNEL_ACCESS_TOKEN')
handler = WebhookHandler('YOUR_CHANNEL_SECRET')


@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    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:
        print("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)

    return 'OK'

# eventにuserの情報がたくさん入っている。
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text=event.message.text))

if __name__ == "__main__":
    app.run()
```


同じフォルダーに以下の二つも用意する。

### requirements.txt(herokuのサーバー上でインストールする必要のあるものを書く)
gunicornは必ずインストールする。(herokuサーバーで必要なライブラリーらしい)
venvというpythonの仮想環境を使うともっとすっきりするが、[直近の記事](https://qiita.com/KKkbt/items/625899c311254fd42e44)でも書いてあるように上手くできなかったのでやらなかった。


```
argon2-cffi==20.1.0
asgiref==3.2.10
async-generator==1.10
attrs==20.2.0
backcall==0.2.0
bleach==3.1.5
certifi==2020.6.20
cffi==1.14.2
chardet==3.0.4
click==7.1.2
colorama==0.4.3
decorator==4.4.2
defusedxml==0.6.0
entrypoints==0.3
Flask==1.1.2
gunicorn==20.0.4
idna==2.10
itsdangerous==1.1.0
jedi==0.17.2
Jinja2==2.11.2
json5==0.9.5
jsonschema==3.2.0
line-bot-sdk==1.17.0
MarkupSafe==1.1.1
mistune==0.8.4
nbclient==0.5.0
nbconvert==6.0.2
nbformat==5.0.7
nest-asyncio==1.4.0
notebook==6.1.4
numpy==1.19.2
packaging==20.4
pandocfilters==1.4.2
parso==0.7.1
pickleshare==0.7.5
prometheus-client==0.8.0
prompt-toolkit==3.0.7
pycparser==2.20
Pygments==2.7.0
pyparsing==2.4.7
pyrsistent==0.17.3
python-dateutil==2.8.1
pytz==2020.1
pywinpty==0.5.7
pyzmq==19.0.2
requests==2.24.0
selenium==3.141.0
Send2Trash==1.5.0
six==1.15.0
sqlparse==0.3.1
terminado==0.8.3
testpath==0.4.4
tornado==6.0.4
urllib3==1.25.10
wcwidth==0.2.5
webencodings==0.5.1
Werkzeug==1.0.1

```

### Procfile

```
web: gunicorn app:app --log-file -
```

以下の手順で操作してherokuにディプロイする！

```
heroku login
```

```
heroku git:clone -a [自分のapp名]
```

```
cd [自分のapp名]
※current directoryで作業してるならやらなくてよい
```

```
git add . #ファイルをaddしてる
git commit -am "make it better" #変更したファイルを更新してる
git push heroku master #pushしてる
```

こんな感じでとりあえずlineにメッセージを送れるようにする。
# 今回自分の作りたいlinebotを作る上での課題
##### Google Map Apiを使ってお店の情報を取ってくる。

解決方法：
とくにない。ただただGoogle Map Apiのドキュメント見るだけ。

##### userの情報を個別に保存する。

解決方法：
これが結構面倒くさかった。最初はデータベース使ってみちゃおうかなと思ったけど、学習コストが大きそうだったので今回はやめた。次にpickleファイルに辞書を保存してやってみたが、なぜか上手く行かず、、。バイナリデータはダメなのか？？原因ははっきりせず。
最終的に辞書をjson形式で保存することにした。上手く行ったが変更を加えるたびにデータが消えてしまう、、。lineでパスワードを入力したらjsonファイルを送ってくるようにしようかと思ったが、今回はやめた。

##### userからのメッセージを受け取って、メッセージに応じた対応をする。
解決方法
これがまた結構大変だった。変数の”登録”、”削除”に加え、自分が作った変数の”確認”とお店・施設の情報の”確認”の四つの応答を作った。（解決方法の説明にはなってないが、下のpythonファイルを見ていただければと思う。）

# 作ったファイル(requirements.txt, Procfile以外)

自分の勉強のためにできるだけ多くのライブラリや文法を盛り込んだつもり。

### app.py
mainファイル。

``` python
import json

class create_reply_message(object):
    user_id = None
    user_dictionaries = None
    the_user_dictionary = None

    def __init__(self, user_id=None,*args, **kwargs):
        super().__init__(*args, **kwargs)
        self.user_id = user_id

        # 辞書の用意
        with open("users_info.json", 'r') as f:
            self.user_dictionaries = json.load(f)
            self.the_user_dictionary = self.user_dictionaries.setdefault(self.user_id, {})

    def legister_fav_shop_institution(self, shop_institute_actual_name, shop_institute_variable):
        user_dictionaries_copy = self.user_dictionaries.copy()
        the_user_dictionary_copy = user_dictionaries_copy[self.user_id]
        the_user_dictionary_copy[shop_institute_variable] = shop_institute_actual_name
        with open("users_info.json", 'w') as f:
            json.dump(user_dictionaries_copy, f)
        return '登録完了!'

    def confirm_what_legistered(self):
        if self.the_user_dictionary == {}:
            reply_msg = "登録したお店・施設はまだないよ、、"
            return reply_msg
        else:
            reply_msg = []
            for dict_key in self.the_user_dictionary:
                reply_msg.append(dict_key)
            reply_msg.insert(0, f'登録したお店・施設は下の{len(self.the_user_dictionary)}つ！！')
            return "\n・".join(reply_msg)

    def delete_fav_shop_institution(self, shop_institute_variable):
        user_dictionaries_copy = self.user_dictionaries.copy()
        the_user_dictionary_copy = user_dictionaries_copy[self.user_id]
        del the_user_dictionary_copy[shop_institute_variable]
        with open("users_info.json", 'w') as f:
            json.dump(user_dictionaries_copy, f)
        return shop_institute_variable + 'を削除完了！'
```
### google_maps_client.py
Google Map Apiから情報をもらって、self.変数に入れるだけのクラス。

```python

import requests
from urllib.parse import urlencode, urlparse, parse_qsl


class locate_fav_shop_institution(object):
    data_type="json"
    location_query = None
    api_key = None

    def __init__(self, api_key=None, shop_institution_name=None,
                 *args, **kwargs):
        super().__init__(*args, **kwargs)
        if api_key == None:
            raise Exception('API key is required')
        self.api_key = api_key
        self.location_query = shop_institution_name

        if self.location_query != None:
            self.place_id = self.extract_place_id()
        if self.place_id == '':
            raise Exception('Your fav shop/instition couldn\'t be located.')
        self.fav_shop_institution_info = self.extract_details()

    def extract_place_id(self):
        base_endpoint_places = f"https://maps.googleapis.com/maps/api/place/findplacefromtext/{self.data_type}"
        params = {
            "key": self.api_key,
            "input": self.location_query,
            "inputtype": "textquery",
             "fields": "place_id"
        }

        params_encoded = urlencode(params)
        places_endpoint = f"{base_endpoint_places}?{params_encoded}"

        r = requests.get(places_endpoint)
        if r.status_code not in range(200, 299):
            return ""
        return r.json()['candidates'][0]['place_id']

    def extract_details(self):
        detail_base_endpoint = f"https://maps.googleapis.com/maps/api/place/details/{self.data_type}"

        detail_params = {
            "place_id": f"{self.place_id}",
            "fields": "business_status,opening_hours,formatted_phone_number,website",
            "language": "ja",
            "key": self.api_key
        }
        detail_params_encoded = urlencode(detail_params)
        detail_url = f"{detail_base_endpoint}?{detail_params_encoded}"
        r = requests.get(detail_url)
        return r.json()['result']
```
### how_to_reply.py
相手からもらったメッセージに応じた対応を出来るようにするためのクラス。

```python

import json

class create_reply_message(object):
    user_id = None
    user_dictionaries = None
    the_user_dictionary = None

    def __init__(self, user_id=None,*args, **kwargs):
        super().__init__(*args, **kwargs)
        self.user_id = user_id

        # 辞書の用意
        with open("users_info.json", 'r') as f:
            self.user_dictionaries = json.load(f)
            self.the_user_dictionary = self.user_dictionaries.setdefault(self.user_id, {})

    def legister_fav_shop_institution(self, shop_institute_actual_name, shop_institute_variable):
        user_dictionaries_copy = self.user_dictionaries.copy()
        the_user_dictionary_copy = user_dictionaries_copy[self.user_id]
        the_user_dictionary_copy[shop_institute_variable] = shop_institute_actual_name
        with open("users_info.json", 'w') as f:
            json.dump(user_dictionaries_copy, f)
        return '登録完了!'

    def confirm_what_legistered(self):
        if self.the_user_dictionary == {}:
            reply_msg = "登録したお店・施設はまだないよ、、"
            return reply_msg
        else:
            reply_msg = []
            for dict_key in self.the_user_dictionary:
                reply_msg.append(dict_key)
            reply_msg.insert(0, f'登録したお店・施設は下の{len(self.the_user_dictionary)}つ！！')
            return "\n・".join(reply_msg)

    def delete_fav_shop_institution(self, shop_institute_variable):
        user_dictionaries_copy = self.user_dictionaries.copy()
        the_user_dictionary_copy = user_dictionaries_copy[self.user_id]
        del the_user_dictionary_copy[shop_institute_variable]
        with open("users_info.json", 'w') as f:
            json.dump(user_dictionaries_copy, f)
        return shop_institute_variable + 'を削除完了！'
```

### personal_informations.py
個人情報が入ってるので掲載はしない。informationという不可算名詞にsをつけてしまった、、はずい、、

### confirm.txt
お店・施設の情報を取ってきたときに送るテンプレート用のテキスト。

```
$nameの様子見てきたよ！
$businessStatus

今週の予定は↓↓
$openingHours

もし何かあったら下の電話番号に連絡！
$phoneNumber

ちなみにホームページは↓↓
$website
```

### explanation.txt
botの説明をするためのテンプレート用のテキスト。

```
$nameさん連絡してくれてありがとう！

$nameさんがこれまで登録したお店・施設を知りたい場合は
"確認"と送ってください！
（例：確認）

$nameさんがこれまで登録してくれたお店・施設の情報を確認したいときは
"確認　[登録名]"と送って下さい！
（例：確認　図書館）

新しくお店・施設の情報を登録したい時は
"登録　[お店・施設の正式名称]　[登録名]"と送って下さい!
（例：登録　東京工業大学付属図書館　図書館）

逆にお店・施設の情報を削除したいときは
"削除　[登録名]"と送ってください!
(例：削除　図書館)

よろしく！！
```

### user_info.json
userの情報を保存するjsonファイル。pickleファイルだと上手く行かなかったのでjsonにした。
できればデータベース使いたかった、、。


```
{user_id: {"\u81ea\u8ee2\u8eca\u5c4b": "\u30b5\u30a4\u30af\u30eb\u30d9\u30fc\u30b9\u3042\u3055\u3072\u4e09\u9df9\u4e95\u53e3\u5e97", "\u3061\u305a\u3051": "\u6771\u4eac\u5de5\u696d\u5927\u5b66\u4ed8\u5c5e\u56f3\u66f8\u9928"}}
```

全然説明になっていないけれどこんな感じで完成させた。初心者なのでPythonなのにかなり読みにくいコードになってしまったが、興味があれば読み解いてほしい。

# 実際に使ってみる

#### 適当なメッセージを送る
適当なメッセージを送るとline-botの説明を返す。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363957/b75a45ba-ac14-a6a9-e6f3-012272da9f08.png)

#### "確認"というメッセージを送る
これまでに登録した名前を確認できる！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363957/531e75f3-ed38-f4e7-5b00-6e236eb31ee2.png)

#### "登録　[正式名称]　[登録したい名前]"というメッセージを送る
新しく自分の好きなお店・施設を登録できる。
必ず間の空白は全角にしなければだめ！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363957/a2b53c6b-30a9-94ca-7b3b-bb2ddb9f8c5f.png)
#### "削除　[登録した名前]"というメッセージを送る
これまでに登録したお店・施設を削除できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363957/78e07cf8-d825-73be-71a2-cffb40431ed5.png)

#### ここでもう一度確認。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363957/1c4241d6-2409-c83f-8e98-d5ea5658fbbc.png)

#### "確認　[登録したお店・施設]"というメッセージを送る
お店が空いているかどうか、営業時間、電話番号、ウェブページを確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363957/d1baa682-a5b4-b9c1-97bf-a7f0a1761cf2.png)


# 課題

- コードが汚い。
- 説明が分かりずらい。
- 知識がまだ少なく（自分の中の）選択肢が少ない。
- エラーを拾えていない（エラーが出ているはずだがどこで出ているかは分からずじまい、、、それでも一応上手く行くのだが見つけて修正したかった。）
- 一定時間(30分)経つとherokuのサーバーがoffになる(お金の問題)。今回jsonファイルでやったためサーバーがオフになると情報がリセットされるという致命的な問題、、。

# 感想

ダメなところなんて挙げ始めたらきりがないけれど、やっぱりものづくりはめちゃめちゃ楽しい！
専攻は機械工学だからプログラミングを書く機会はそこまで多くないし、これから研究室関連の授業がきてかなり忙しくなって、他のことを勉強している暇はないかもしれないけれど、アイデアを出したり、ものづくりという点では同じで、きっとためになると思う。そして何より楽しいからこれからもプログラミングを続けていきたいと思う。

# 最後に
↓が作ったline-botです。初心者が作ったものだし、エラーも沢山あるけど（正直何個かわかってるけど直してないところあり、、笑）一生懸命作りました！友達追加してくれると嬉しいです。
![messageImage_1602075610648.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363957/7a0d7e2d-9608-367e-9061-ffc9bcc5a63f.jpeg)