LINE WORKSのAPI 2.0 (Beta) を使って画像メタ情報取得Bot作ってみた

# はじめに
2021/10/28よりLINE WORKSのAPI 2.0 (Beta) が提供開始された。

https://jp1-notice.worksmobile.com/view/#/detail-view/548?usess=1&serviceType=12&serviceLanguage=ja_JP

上のリリースページにもあるように、全体的にシンプルな作りとなったり、OAuth2.0による認可機能が実装されたり等、多くのアップデートが入った。

今回はこのAPI 2.0 (Beta)を使ったBotの作成や画像ダウンロード周りを試すため、送られてきた画像のメタ情報を返すシンプルなトークBotを作成してみる。

## 関連: 従来のAPIを使ったトークBot作成
以前従来のAPIでトークBotを作成した時の過去記事

https://qiita.com/mmclsntr/items/1d0f520f1df5dffea24b

# 注意
**本記事ではベータ版の情報を扱っています。今後のアップデートや正式版では一部異なる仕様となる可能性があります。**

# Bot作成
## 0. 前準備
### Developer Consoleでアプリを登録
従来のAPIでは、Developer Consoleでテナント単位の各種キーや証明書を管理・発行するようになっていたが、API 2.0 (Beta)では「アプリ」という概念が追加され、アプリ単位に認証情報を管理するようになった。

https://developers.worksmobile.com/jp/reference/client-app?lang=ja


アプリ設定画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241369/fbedb2cb-9dbd-c260-9cdc-8c100af23859.png)

### アプリ画面で各種キーの発行
従来のAPIには「サービスAPI」と「サーバーAPI」の2種類があり、トークBot関連はサーバーAPIに区分されていた。それぞれで必要なキー情報も異なっていたが、API 2.0 (Beta)ではその２種類が1つに統合され区別はなくなった。

アプリを作成すると、アプリ単位に持っている「Client ID」と「Client Secret」が生成される。

- Client ID
    - アプリを識別するID
- Client Secret
    - アプリごとに持っている、Client IDに紐づくシークレット値

これらは、APIの認証で利用するAccess Tokenの取得のために利用される。
また、Access Tokenの取得のために加えて「Service Account」と「Private Key」も取得する。

- Service Account
    - アプリ専用の仮想管理者アカウント。ユーザーアカウントの代わりにAccess Tokenを取得することが可能となる。
- Private Key
    - 上記Service Accountを使用してAccess Tokenを取得する際に必要な認証キー。

最後に、「OAuth Scopes」で。そのアプリの権限範囲を設定する。

- OAuth Scope
    - アプリが利用するリソースとその操作権限。
    - https://developers.worksmobile.com/jp/reference/scopes?lang=ja 

今回はトークBotの利用のため、Scopeは `bot` を設定しておく。

追記 2/26
当初、Scopeは `bot, bot.read` としていましたが、コメントにて、botはbot.readも内包しているとご指摘いただきました。修正しました。

> 下位resourceがある場合は「.(dot)」区切りで表現されます。上位resourceは、下位のresourceも保有します。
例えば、group resourceはfolderを含む上位resourceになります。
group
group.folder
https://developers.worksmobile.com/jp/reference/scopes?lang=ja 


## 1. チャットボットサーバーの構築・実装
トークBotの本体となるチャットボットサーバーを構築・実装する。

従来のAPIと同様に、Bot設定のCallback URLに指定したURLに対してHTTP POST リクエストとしてメッセージイベントが送られる (Botの設定については後述)。

https://developers.worksmobile.com/jp/reference/bot-callback?lang=ja

### 構成
今回は、Google Cloudの「Cloud Run」をサーバーとして置いた構成で作成した。

![lineworks-bot-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241369/e71de357-7da0-fcfb-1012-32020917065b.png)

LINE WORKS側から送られてくるメッセージイベントをCloud Runで受信し、それに応じた返答をメッセージ送信用のAPIで返す。また、APIの利用で必要なAccess Tokenの発行・更新についてもCloud SchedulerをトリガーとしてCloud Runで行うようにした。関連する秘匿情報はSecret Managerで管理することとした。

### アプリケーション
今回は、アプリケーションはPythonで実装し、サーバーにはFast APIを利用した。

- Python: 3.8
- [Fast API](https://fastapi.tiangolo.com/ja/): 0.70.0

### Access Tokenの取得
APIを利用するため、まずはAccess Tokenを取得する必要がある。
Service Account (上記で作成したアプリ専用の仮想管理者アカウント) を利用するため、Service Account認証 (JWT) を利用してAccess Tokenを取得する。

https://developers.worksmobile.com/jp/reference/authorization-sa?lang=ja

まず、Client IDとService Account IDとPrivate KeyでJWTを取得する。         

```python
def get_jwt(client_id, service_account_id, privatekey):
    """
    LINE WORKS アクセストークンのためのJWT取得
    """
    current_time = datetime.now().timestamp()
    iss = client_id
    sub = service_account_id
    iat = current_time
    exp = current_time + (60 * 60) # 1時間

    jwstoken = jwt.encode(
        {
            "iss": iss,
            "sub": sub,
            "iat": iat,
            "exp": exp
        }, privatekey, algorithm="RS256")

    return jwstoken
```

取得したJWTと、Client ID, Client Secretを使ってAccess Tokenを取得する。
その際、Scopeを指定する (今回は `bot`)

```python
def get_access_token(client_id, client_secret, scope, jwttoken):
    """アクセストークン取得"""
    url = 'https://auth.worksmobile.com/oauth2/v2.0/token'

    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    params = {
        "assertion": jwttoken,
        "grant_type": urllib.parse.quote("urn:ietf:params:oauth:grant-type:jwt-bearer"),
        "client_id": client_id,
        "client_secret": client_secret,
        "scope": scope,
    }

    form_data = params

    r = requests.post(url=url, data=form_data, headers=headers)

    body = json.loads(r.text)

    return body
```

Access Tokenの有効期限は24時間固定となっている。
API 2.0 (Beta)では、Access Tokenの取得時のレスポンスに含まれるRefresh Tokenを使ってAccess Tokenの再発行ができる。

```python
def refresh_access_token(client_id, client_secret, refresh_token):
    """アクセストークン更新"""
    url = 'https://auth.worksmobile.com/oauth2/v2.0/token'

    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    params = {
        "refresh_token": refresh_token,
        "grant_type": "refresh_token",
        "client_id": client_id,
        "client_secret": client_secret,
    }

    form_data = params

    r = requests.post(url=url, data=form_data, headers=headers)

    body = json.loads(r.text)

    return body
```

### チャットボットの実装
LINE WORKSから送られてくるイベントを受けてその内容に応じて返答するチャットボット部分について実装する。

#### 今回のチャットボット
今回は、送信された画像のサイズや種類、メタ情報を返すチャットボットを作る。

```text:流れ
① メッセージ受信
↓
② 画像を取得
↓
③ 画像分析 (メタ情報等取得)
↓
④ 結果を返信 (通知)
```

#### ① メッセージ受信

POSTリクエストで受信し、リクエストボディに以下のドキュメントにあるフォーマットでメッセージについての情報が格納されている。

https://developers.worksmobile.com/jp/reference/bot-callback?lang=ja

##### (補足) 改ざんチェックについて
既存のAPIでは、送られてくるリクエストの改ざんチェックを行うために、署名検証の仕組みが用意されている。
API 2.0 (Beta)では現在利用できないが、正式版では実装される予定となっている。

>Beta版ではメッセージの改ざん検証は利用できません。
正式版リリース時に対応を予定しております。

#### ② 画像の取得
トークから画像が送信された際にサーバーが受信するメッセージの形式は以下のようになっている。

```json
{
    "type":"message",
    "source":{
        "userId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    },
    "createdTime":1640281225908,
    "content":{
        "type":"image",
        "fileId":"jp1.1640281225928569422.1640367625.2.3022841.10353252.125185387.11"
    }
}
```
参考

https://developers.worksmobile.com/jp/reference/bot-callback-message?lang=ja

ここには画像データは含まれておらず、含まれている`content.fileId`を使って、以下のコンテンツダウンロードAPIから画像をダウンロードする。

https://developers.worksmobile.com/jp/reference/bot-attachment-get?lang=ja

```python
def get_attachments(bot_id, file_id, access_token):
    """コンテンツダウンロード"""
    url = "https://www.worksapis.com/v1.0/bots/{}/attachments/{}".format(bot_id, file_id)

    headers = {
          'Authorization' : "Bearer {}".format(access_token)
        }

    r = requests.get(url=url, headers=headers)

    r.raise_for_status()

    return r.content
```

実際は、まずはリダイレクトURLが取得され、そこに転送することでファイルデータを取得することができる。
使っているrequestsモジュールでは自動的にリダイレクトがされるため上記のような書き方となる。

#### ③ 画像分析 (メタ情報等取得)
上記で取得した画像データから、送信された画像のメタ情報などを取得する。今回はpillowモジュールを利用した。

https://python-pillow.org/

コンテンツダウンロードAPIでバイナリデータとして取得されるため、これをioモジュールと組み合わせて読み込ませて情報を取得する。

```python
from PIL import Image
import io
...
img = Image.open(io.BytesIO(img_data))
msg = "Size: {}\nFormat: {}\nMode: {}\nInfo: {}".format(img.size, img.format, img.mode, img.info)
```

#### ④ 結果を返信 (通知)
画像の情報をトークに返信する。

テキストメッセージを返すため、contentのフォーマットは以下の通り。

```json
{
  "content": {
    "type": "text",
    "text": msg
  }
}
```

返信はメッセージ送信APIで行う。

https://developers.worksmobile.com/jp/reference/bot-user-message-send?lang=ja

BotIDや、受信メッセージに含まれているUser ID、Access Tokenを使って送信する。

```python
def send_message(content, bot_id, user_id, access_token):
    """メッセージ送信"""
    url = "https://www.worksapis.com/v1.0/bots/{}/users/{}/messages".format(bot_id, user_id)

    headers = {
          'Content-Type' : 'application/json',
          'Authorization' : "Bearer {}".format(access_token)
        }

    params = content
    form_data = json.dumps(params)

    r = requests.post(url=url, data=form_data, headers=headers)

    r.raise_for_status()
```
### デプロイ
実装後、Cloud Runへデプロイし、また、その他認証に必要な情報についてもSecret Managerに登録する。

## 2. Botの登録と公開
従来通り、Developer Consoleの「Bot」で登録する。

Bot画面

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241369/4561b1d2-22b0-414f-d2ee-748e45ac3ed9.png)

その際に、「API Interface」の設定項目で、「従来のAPI」または「API 2.0 (beta)」を選択できるが、今回は、「API 2.0 (beta)」を選択する。
また、Callback URLにCloud RunのURLを登録し、チャットボットサーバーへメッセージイベントが送信されるようにする。

登録後、LINE WORKSのテナントの管理画面でBotを追加し、公開となる。

## 3. できたもの
![IMG_1043.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241369/851226b4-e441-a933-6db9-d327f67e8423.jpeg)

PNG画像を送った場合の結果。画像のサイズや種類等をテキストメッセージとして返答する。
(サイズにもよるが) 画像のダウンロードに時間がかかるようで、１分後に返信がくる。

## 4. ソースコード
以下、本チャットボットアプリのソースコード

https://github.com/mmclsntr/lw-bot-image-meta-info

# まとめ
API 2.0 (Beta) となってAPIの全体の構成や認証周りが特に変わったが、従来のAPIと変わらずチャットボットを作る上で必要そうなAPIはしっかり揃っており、よりシンプルになったと感じた。

Beta版としてまだ十分に安定していないところもあるようなので、こちらもフィードバックを送りつつ正式版を期待したいと思う。