LINE Messaging APIのリッチメニューをPythonで操作する

LINE Messaging APIのリッチメニューを操作するためのAPIが公開されました。リッチメニューを自由自在かつ用途に応じてダイナミックに変更できるなど、超絶進化を遂げたので使わない手はありません。

（公式）リッチメニューを使う - LINE Developers
https://developers.line.me/ja/docs/messaging-api/using-rich-menus/ 

さて、このAPIの使い方は上のURLの通りなのですが、ちょうどPythonの実装作って試してみたのと公式SDKにまだ含まれていなかったので、せっかくなので公開します。例外処理などは一切行っていませんので、その辺は適宜やっていただければと。

GitHubにも放流しました。コード見ればわかるという方はこちらからでも。
https://github.com/uezo/richmenu

# 依存ライブラリのインポート

ユーティリティクラス用に`richmenu.py`を作成したら、`json`と`requests`をimportします。LINEのSDKは使っていません。

```Python:richmenu.py
import json
import requests
```

# リッチメニューのデータモデルの作成

そんなに複雑じゃないのでクラスを作らなくてもいいかな・・・と思ったのですが、エリアを追加するとき見やすい方がいいので、そのための`add_area`を備えた以下のクラスを作成します。

```Python:richmenu.py
class RichMenu:
    def __init__(self, name, chat_bar_text, size_full=True, selected=False):
        self.size = {"width": 2500, "height": 1686}
        if not size_full:
            self.size["height"] = 843
        self.selected = selected
        self.name = name
        self.chat_bar_text = chat_bar_text
        self.areas = []

    def add_area(self, x, y, width, height, action_type, payload):
        bounds = {"x": x, "y": y, "width": width, "height": height}
        action = {"type": action_type}
        if action_type == "postback":
            if isinstance(payload, list):
                action["data"] = payload[0]
                if len(payload) > 1:
                    action["text"] = payload[1]
            else:
                action["data"] = payload
        elif action_type == "uri":
            action["uri"] = payload
        else:
            action["text"] = payload
        self.areas.append({"bounds": bounds, "action": action})

    def to_json(self):
        dic = {"size": self.size, "selected": self.selected, "name": self.name, "chatBarText": self.chat_bar_text, "areas": self.areas}
        return json.dumps(dic)
```

# リッチメニュー操作用クラスの作成

APIの操作は一通りラッピングしているはずです。リッチメニューの登録は生のAPIだとメニューの定義の登録→画像のアップロード・・・と2段階になりますが、せっかくなので`register`メソッド一発で登録できるようにしました。あと、テスト用にたくさん登録すると削除が面倒なので、一括削除の`remove_all`メソッドも用意しておきましたのでご利用ください。

```Python:richmenu.py
class RichMenuManager:
    def __init__(self, channel_access_token, verify=True):
        self.headers = {"Authorization": "Bearer {%s}" % channel_access_token}
        self.verify = verify

    def register(self, rich_menu, image_path=None):
        url = "https://api.line.me/v2/bot/richmenu"
        res = requests.post(url, headers=dict(self.headers, **{"content-type": "application/json"}), data=rich_menu.to_json(), verify=self.verify).json()
        if image_path:
            self.upload_image(res["richMenuId"], image_path)
        return res

    def upload_image(self, rich_menu_id, image_path):
        url = "https://api.line.me/v2/bot/richmenu/%s/content" % rich_menu_id
        image_file = open(image_path, "rb")
        return requests.post(url, headers=dict(self.headers, **{"content-type": "image/jpeg"}), data=image_file, verify=self.verify).json()

    def download_image(self, richmenu_id, filename=None):
        url = "https://api.line.me/v2/bot/richmenu/%s/content" % richmenu_id
        res = requests.get(url, headers=self.headers, verify=self.verify)
        if filename:
            with open(filename, "wb") as f:
                f.write(res.content)
        else:
            return res.content

    def get_list(self):
        url = "https://api.line.me/v2/bot/richmenu/list"
        return requests.get(url, headers=self.headers, verify=self.verify).json()

    def remove(self, richmenu_id):
        url = "https://api.line.me/v2/bot/richmenu/%s" % richmenu_id
        return requests.delete(url, headers=self.headers, verify=self.verify).json()


    def remove_all(self):
        menus = self.get_list()
        for m in menus["richmenus"]:
            self.remove(m["richMenuId"])

    def apply(self, user_id, richmenu_id):
        url = "https://api.line.me/v2/bot/user/%s/richmenu/%s" % (user_id, richmenu_id)
        return requests.post(url, headers=self.headers, verify=self.verify).json()

    def detach(self, user_id):
        url = "https://api.line.me/v2/bot/user/%s/richmenu" % user_id
        return requests.delete(url, headers=self.headers, verify=self.verify).json()

    def get_applied_menu(self, user_id):
        url = "https://api.line.me/v2/bot/user/%s/richmenu" % user_id
        return requests.get(url, headers=self.headers, verify=self.verify).json()
```
# 使ってみる

事前にLINE DevelopersでChannel Access Tokenを確認しておくのと、LINE@Managerでリッチメニューを有効にしておく必要があるようです。準備が整ったら`example.py`を作成して先ほど作ったクラスをimportします。

```Python:example.py
from richmenu import RichMenu, RichMenuManager
```

次に、チャネルアクセストークンを使ってRichMenuManagerのインスタンスを生成。もしrequestsのバージョンが古いなどでSSL関連のエラーが出る場合は、コンストラクタの第2引数に`verify=False`を渡して見てください。（自己責任でお願いします）

```Python:example.py
# Setup RichMenuManager
channel_access_token = "YOUR_CHANNEL_ACCESS_TOKEN"
rmm = RichMenuManager(channel_access_token)
```

続いて、リッチメニューの定義を作成します。上下に4分割して、テキスト、URL、ポストバック、ポストバック（メッセージつき）の4種類のアクションを実行するエリアを作成しました。

```Python:example.py
# Setup RichMenu to register
rm = RichMenu(name="Test menu", chat_bar_text="Open this menu")
rm.add_area(0, 0, 1250, 843, "message", "テキストメッセージ")
rm.add_area(1250, 0, 1250, 843, "uri", "http://imoutobot.com")
rm.add_area(0, 843, 1250, 843, "postback", "data1=from_richmenu&data2=as_postback")
rm.add_area(1250, 843, 1250, 843, "postback", ["data3=from_richmenu_with&data4=message_text", "ポストバックのメッセージ"])
```

ここまできたらいよいよLINEにリッチメニューを登録します。この段階ではまだ登録されるだけでどのユーザーにも表示されませんのでご安心ください。また、実行前にメニューの画像を準備するのをお忘れなく。なおこの画像はきっかり2500*1686 or 843じゃないとダメで、自動で拡大してくれません。

<img src="https://qiita-image-store.s3.amazonaws.com/0/53296/cbab2e08-fedb-5fdb-b9c0-5181a853035e.png" width=350 />

```Python:example.py
# Register
res = rmm.register(rm, "/path/to/menu.png")
richmenu_id = res["richMenuId"]
print("Registered as " + richmenu_id)
```

リッチメニューIDが取得できたら成功です。最後にユーザーに紐付けすることで、直ちにユーザーの画面に反映されます。これは本当に「直ちに」で、APIを呼び出した瞬間に画面が切り替わるので見ていておおってなります。

```Python:example.py
# Apply to user
user_id = "LINE_MID_TO_APPLY"
rmm.apply(user_id, richmenu_id)
```

自分のアカウントであれば画面で確認できますが、他人のアカウントにちゃんと反映できたかは以下で確認できます。

```Python:example.py
# Check
res = rmm.get_applied_menu(user_id)
print(user_id  + ":" + res["richMenuId"])
```

その他、登録済みメニュー一括取得、メニュー画像のダウンロード、適用解除、削除、登録済みメニュー一括削除は以下のように呼び出すことができます。

```Python:example.py
# Others
res = rmm.get_list()
rmm.download_image(richmenu_id, "/path/to/downloaded_image.png")
res = rmm.detach(user_id)
res = rmm.remove(richmenu_id)
rmm.remove_all()
```

# おわりに

リッチメニューを動的に切り替えるようなLINE BOTはまだあまり見たことがないので、それだけでもお友達に差をつけられるかもしれませんね。ただ、以下のような要検討事項があると思いますので、よくUXを試してみる必要がありそうです。

- 動的に変更しても、元の状態に戻すトリガーが発生するとは限らない（途中で会話を放棄とか）
- どのユーザーにどのメニューが適用されているかは自身で管理が必要

それでは、エンジョイLINEBOT開発！