LINEのMessaging APIを用いて能動的通知をする方法

### これは何
アプリケーションからLINE連携をしてMessaging API経由でLINEにメッセージを送る方法。
特に、LINEのルームで話しかけられた時の自動返信BOT”ではなく”、こちらから任意のタイミングで能動的にメッセージを送る方法について。

### 概要
* LINEのDeveloper登録する
* Developer Tool 上でログインチャンネルとMessaging APIチャンネルを作る
* LINEのOAuth認証をしてもらって、ユーザのLINE IDをアプリケーション側で知る
* 友達追加してもらう
* LINE@ のmessaging APIを使えるプランに入る
* Messaging APIを使って通知する

### LINEのDeveloper登録する
https://developers.line.biz/ja/ 
ここから適当なLINEアカウントで開発者登録をする。アカウントさえあれば流れに任せて作れたので略。

### Developer Tool 上でログインチャンネルとMessaging APIチャンネルを作る
* プロバイダーを作成
    * プロバイダーはアプリ提供者の単位っぽい。会社名とかで適当に命名する
* プロバイダーの中にチャンネルを作る
    * ここでいうチャンネルは、友達ルームというようなニュアンスではなくて、ログイン機能を提供する機能のチャネル、メッセージ送信機能を提供する機能チャネルみたいなイメージっぽい

![貼り付けた画像_2019_04_18_14_10.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144064/e7dead9f-97f5-4154-0b5f-01aa2288e652.jpeg)


### LINEのOAuth認証をしてもらって、ユーザのLINE IDをアプリケーション側で知る
参考: ここら辺の公式ページを参考に進める https://developers.line.biz/ja/docs/line-login/web/integrate-line-login/

```html

<a class="btn line-btn" href="https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id={{ $lineSetting->getLoginChannelId() }}&redirect_uri={{ $encodedBaseUrl }}&state={{ csrf_token() }}&scope=openid">LINEログインする</a>
```

* response_type=code は認証した後のコールバックで?code=hogehoge というパラメタでcodeを返してくれる方式。このCodeがAPIを叩くためのTokenを取得するためのワンタイムコードとして使える
* client_idは上で作ったチャンネルに書かれている
* redirect_url はあらかじめログインチャンネルでも設定しておく必要があり、ここのリンクのパラメタで指定したものとチャンネルで設定したものが一致すればちゃんとログインページが表示されるはず。パラメタの方には urlencode() 関数でエンコードしたものを使います。
* stateはCSRF対策のための文字列。コールバックで戻ってきた時に正当性のチェックのために使われる。私の環境ではlaravelを使っているので、csrf_token()を使って、コールバックで戻ってきた時に再度セッションに入っているものと比べることにより検証をしました
* scope=openid は、今回プロフィールの情報とかいらないのでIDだけもらえるような権限を要請するためのもの

##### 以下はコールバックで戻ってきた時の処理
##### stateで設定したcsrf_token()の検証
```php
if (!$request->input('state') === csrf_token()) {
    throw new Exception();
}
```

##### codeを用いてIDを取得
guzzleを用いてcodeを元にIDを要求するリクエストをしています。

```php
class LineLoginApiClient
{
    private $guzzleClient;

    public function __construct(Client $guzzleClient)
    {
        $this->guzzleClient = $guzzleClient;
    }

    public function getLineId(AuthTemporaryCode $code, AuthCallbackedUrl $callbackUrl): LineId
    {
        // callbackUrlはあらかじめLine Developers管理画面から設定しておく
        // 認可コード($code)をもらったCallbackURLとtoken apiで使用するセットが違うと失敗するので注意
        $response = $this->guzzleClient->request(
            'POST',
            'https://api.line.me/oauth2/v2.1/token',
            [
                'form_params' => [
                    'grant_type' => 'authorization_code',
                    'code' => $code->rawValue(),
                    'redirect_uri' => $callbackUrl->rawValue(),
                    'client_id' => 設定から読み込む,
                    'client_secret' => 設定から読み込む
                ],
                'headers' => ['Content-Type: application/x-www-form-urlencoded']
            ]
        );

        $decodedResult = json_decode($response->getBody()->getContents(), true);

        // レスポンスのJWTの中のidTokenにIDが含まれていたので、Tokenは破棄してこれで目的を達成する（プロフィール情報取得とかを継続的にやるならTokenも残しておく想定）
        $idTokenJWT = $decodedResult['id_token'];
        // JWTの仕様により、ピリオドで区切られらた文字列の二番目がbase64encodeされたペイロード
        $jwtPayload = json_decode(base64_decode(explode('.', $jwt)[1]), true);$idTokenJWT);

        return new LineId($jwtPayload['sub']);
    }
}
```

ここで得たLINEのユーザIDをDBなどに保存しておく。IDはU214sdgret234 みたいなUから始まる文字列。

### 友達追加してもらう
https://admin-official.line.me/
のページのアカウント設定から、LINE友達追加ボタンがあるのでそれを自分のサイトの適当なところに置いておくと便利。
![貼り付けた画像_2019_04_18_14_30.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144064/bb33ad92-fc1d-627c-be6d-b8fa883bc6a5.jpeg)


### LINE@ のmessaging APIを使えるプランに入る
https://at.line.me/jp/plan
~~ここに書いてあるMessaging APIのDeveloper Trialかプロプラン。
無料プランだとPUSHメッセージはできないので注意。~~

ちょうどこの記事を書いたくらいの時期から新しいプランができているようです。最新情報については公式の資料からキャッチをお願いします。
https://www.linebiz.com/lineat_migration/


### Messaging APIを使って通知する
ここら辺のドキュメントを参考にして作る
https://developers.line.biz/ja/reference/messaging-api/

sdk使うためにcomposerでインストールしておく。 参考: https://github.com/line/line-bot-sdk-php

```
$ composer require linecorp/line-bot-sdk
```


```php
class LineMessageApiClient
{
    private $guzzleClient;

    public function __construct(Client $client)
    {
        $this->guzzleClient = $client;
    }

    // LineIDを引数にもらって送る関数。LineIdとかLineMessageText型はバリューオブジェクトとして私が作ったものなので注意
    public function sendMessage(LineId $lineId, LineMessageText $text)
    {
        // ここについては公式のライブラリがあったので、guzzleではなくてそれを使ってます
        $messenger = new LINEBot(
            new LINEBot\HTTPClient\CurlHTTPClient($this->getMessageChannelToken()->rawValue()),
            [
                'channelSecret' => config('line.message_channel_secret')
            ]
        );

        $messenger->pushMessage($lineId->rawValue(),
            new LINEBot\MessageBuilder\TextMessageBuilder($text->rawValue()));
    }

    private function getMessageChannelToken(): LineMessageChannelToken
    {
        $channelTokenResponse = $this->guzzleClient->post('https://api.line.me/v2/oauth/accessToken',
            [
                'form_params' => [
                    'grant_type' => 'client_credentials',
                    'client_id' => config('line.message_channel_id'),
                    'client_secret' => config('line.message_channel_secret')
                ],
                'headers' => ['Content-Type: application/x-www-form-urlencoded']
            ]
        );
        $channelTokenJson = json_decode($channelTokenResponse->getBody()->getContents(), true);

        $channelToken = $channelTokenJson['access_token'];
        // キャッシュなどに入れるなら有効期限はこれを使う
        $secondsToExpire = $channelTokenJson['expires_in'];

        return new LineMessageChannelToken($channelToken);
    }
```  

おまけ
verifyするなら。

```php
    private function verifyChannelToken(string $token): bool
    {
        $response = $this->guzzleClient->post('https://api.line.me/v2/oauth/verify',
            [
                'form_params' => [
                    'access_token' => $token
                ],
                'headers' => ['Content-Type: application/x-www-form-urlencoded']
            ]
        );

        // 有効なTokenの場合200, 有効ではない場合は400が帰ってくる
        return $response->getStatusCode() === 200;
    }

```