LINE Messaging API SDK(Node.js)クックブック

# はじめに
最近LINEを使って色々と作業したので、備忘録的に[LINE Messaging API SDK(Node.js)](https://github.com/line/line-bot-sdk-nodejs)の
使い方を記載します。
(TypeScriptで書いていますので、JavaScriptしか分からないという方はすみません。)

基本的には以下の公式ページを参考にすれば書けると思いますが、見てもよく分からんという方の参考になればいいなと思います。

[LINE MessagingAPIリファレンス](https://developers.line.biz/ja/reference/messaging-api/)

# 準備
この章ではLINE MessagingAPIを使用するための準備をします。
準備として、Bot機能を使用し、おうむ返しBotを作ります。

## 環境構築
まずはじめに環境構築から始めます。
最初に`express`を使用して、最少構成のサーバを立てます。

```bash
mkdir <適当な名前>
cd <適当な名前>
npm init -y 
npm install express
npm install -D typescript  ts-node @types/express ngrok
npx tsc --init
touch index.ts
```

> index.ts

```typescript
import express from "express";

const app = express();

const port = process.env.PORT || 8080; // port番号を指定

app.post("/webhook", (req, res) => {
  res.send("準備中");
});

app.listen(port);

console.log("listen on port " + port);
```

ついでに`package.json`の`scripts`に`"start":"ts-node index.ts"`を追加しましょう。

> package.json

```json
{
  "scripts": {
    "start": "ts-node index.ts"
  },
  "dependencies": {
    "express": "^4.17.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.12",
    "ngrok": "^4.0.1",
    "ts-node": "^10.0.0",
    "typescript": "^4.3.2"
  }
}
```

最後に確認します。

```bash
npm start
//別ターミナルを開き以下、コマンド実行
npx ngrok http 8080
// httpsのURLを取得して、以下のコマンドを実行
curl --request POST '<取得したURL>/webhook'
```

`準備中`と返信が返って来ればOKです。

## [LINE Messaging API SDK(Node.js)](https://github.com/line/line-bot-sdk-nodejs)の準備
とりあえずinstallしましょう。

```bash
npm install @line/bot-sdk
```

次に以下のように`index.ts`を変更します。

```typescript
import express from "express";
const app = express();

const port = process.env.PORT || 8080;

// ~~~次からこれより上を省略します。

import * as line from "@line/bot-sdk";

const config = {
  channelAccessToken:<ボットのアクセストークン>, // コードにベタガキしない方がいいです。 
  channelSecret: <ボットのシークレットId>, // コードにベタガキしない方がいいです。
};

app.post(
  "/webhook",
  line.middleware(config),
  (req, res) => {
    res.status(200).send("ok");
  }
);

// ~~~次からこれより下を省略します。
app.listen(port);

console.log("listen on port " + port);
```

これで、以下のコマンドを実行してサーバを立てましょう。

```bash
npm start
//ターミナルを新しく開いて 
npx ngrok http 8080
// 前回の処理が残っていて、8080ポートが使用できない場合は、前回の処理を止めてControl - cで終了して、再実行してください。
// 知識のある方はnodemonなどをinstallしてhotreload機能を使ってください。
```

[LINE Developpers](https://developers.line.biz/)にログインして`ngrokで取得したURL/webhook`を入力して、検証を押して
成功が返ってくるか確かめてください。成功が返って来れば準備は完了です！
![line.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/951917/e4d0d27d-60ca-d437-5ac6-1afc8363541a.png)

## おうむ返しBotの作成
Botに何か話しかけると同じ言葉を返すおうむ返しBotを作成します。
すでに先ほど立てたサーバとLINEとをwebhookで連携できることが確認したので、
あとは、受け取ったデータをどのように返すのかの処理を記述します。

LINEから送ってくる処理すべきリクエストの単位を`event`と呼びます。(公式な呼び方かどうかは分かりません。)
1つのhttpリクエストに複数のeventがやってくる場合があるので、まずは`event`のハンドリングを行います。
そしてその`event`がユーザが発したメッセージの場合だけ、おうむ返しを実行します。
実際のコードは以下です。

> index.ts

```typescript
// ~~省略してます
import * as line from "@line/bot-sdk";
import { TextMessage, WebhookEvent } from "@line/bot-sdk";

const config = {
  channelAccessToken:<ボットのアクセストークン>,
  channelSecret: <ボットのシークレットId>,
};

/*
  色々な処理を行うclientを準備する。
  このclientに処理を命令することによって、Botがアクションを起こす。
*/
const client = new line.Client(config);

app.post("/webhook", line.middleware(config), (req, res, next) => {
  /*
   eventはbodyの中に格納されており
   Promise.allを使用して、複数のevent処理を行う。
  */ 
  Promise.all(req.body.events.map(handleEvent))
    .then((result) => res.json(result))
    .catch((err) => next(err));
});

const handleEvent = async (event: WebhookEvent) => {
  /*
   色々なタイプのイベント毎に処理を振り分ける関数
  */
  if (event.type === "message" && event.message.type == "text") {
    /*
      メッセージが送信されてきた時の処理
    */
    const replyToken = event.replyToken
    const message = event.message.text;
    const response: TextMessage = {
      type: "text",
      text: message,
    };
    await client.replyMessage(replyToken, response);
    return;
  }
};
// ~~省略してます
```

メッセージeventには`replyToken`というものが設定されており、この`replyToken`を使用することによって
発言者に対して、返信することができます。
(event全てにreplyTokenがあるわけではなく、eventのタイプによってはreplyTokenがないものもあります。)

上記のコードで、おうむ返しのBotが出来上がったと思います。実際に試して確認してみてください。
※ 上手くいかない人は`npm start`と`npx ngrok http 8080`をやり直して、[LINE Developpers](https://developers.line.biz/)のwebhook設定をやり直してみてください。

# クックブック
ここからは、まとまった内容というよりは、ユースケースに合わせた内容を記載していきます。
困っている方の助けになればいいなと思います。

## 新規に公式Botアカウントがフォローされた時の処理
Botが新しいユーザにフォローされた時に、特別な処理を行いたい時があると思います。
Botが新しいユーザにフォローされたタイミングでもeventが発行され、webhookを通じて
サーバが知ることができます。

> index.ts

```typescript
const handleEvent = async (event: WebhookEvent) => {
  if (event.type === 'follow') {
    /*
      友達登録された時の処理
    */
    const userId = event.source.userId;
    const profile = await client.getProfile(userId!);
    console.log(profile.displayName);
    return;
  }
 // ~~~他の処理 
};
```

eventを処理する`handleEvent`関数内で、`event.type`が`follow`のものを
検知して、フォローされた際のアクションを行うことができます。
`userId`は`event.source.userId`から得ることができ、`userId`からユーザの詳細情報の
`profile`を得ることもできます。

## クイックリプライを送る
[クイックリプライ](https://developers.line.biz/ja/docs/messaging-api/using-quick-reply/)の詳細についてはリンク先を参照してください。
ざっくりいうとユーザが簡単に返信できるようにするための機能です。
ここでは、テキストメッセージにクイックリプライを設定する方法を示します。
以下は、おうむ返しを行うBotに「yes」,「no」とクイックリプライを設定しています。
クイックリプライは色々な[アクション](https://developers.line.biz/ja/docs/messaging-api/actions/)に対応しているのですが、今回は`postback`アクションというものを設定しています。

```typescript
const handleEvent = async (event: WebhookEvent) => {
  if (event.type === "message" && event.message.type == "text") {
    /*
      メッセージが送信されてきた時の処理
    */
    const message = event.message.text;
    const quickReplys: QuickReplyItem[] = [
      {
        type: "action",
        action: {
          type: "postback",
          label: "yes",
          data: '{"action":"yes"}',
        },
      },
      {
        type: "action",
        action: {
          type: "postback",
          label: "no",
          data: '{"action":"no"}',
        },
      },
    ];
    const response: TextMessage = {
      type: "text",
      text: message,
      quickReply:{
       items :quickReplys
      }
    };
    await client.replyMessage(event.replyToken, response);
    return;
  }
};
```

postbackアクションはかなり有用なアクションで、ボタンを押すとdataに記載されている情報とともに
サーバに送信されます。
※ postbackアクションのdataには好きな文字列を設定することができます。私的におすすめなのはJSON形式で書くとbackendで使いやすくなります。

postbackの受け取りについては次の節で説明します。

## postbackアクションを受け取る
postbackアクションを設定しているボタンなどがクリックされるとpostbackeventが発行されます。
その受け取り方について示します。

```typescript
const handleEvent = async (event: WebhookEvent) => {
  /*
    postBackアクションが送られてきた時の処理
  */
  if (event.type === "postback") {
    const data = event.postback.data;
    const parseData = JSON.parse(data);
    console.log(parseData);
    return;
  }
 // ~~~他の処理は省略
}
```
他の処理と同様にevent.typeでeventをハンドリングします。
`postback`アクションのtypeは、`"postback"`になります。
data自体は文字列型なのですが、JSON形式の文字列型に設定することで、parseして扱いやすくなります。

## pushMessgeを送信する
Botから特定のユーザに向けてメッセージを送信する。
この処理は送りたいuserのIDが分かっている場合に使用します。

> pushMessage.ts

```typescript
import * as line from "@line/bot-sdk";
import { TextMessage } from "@line/bot-sdk";
const config = {
  channelAccessToken:<ボットのアクセストークン>,
  channelSecret: <ボットのシークレットId>,
};
// ~~~ 次回以降省略します。

const client = new line.Client(config);

const pushMessage = async (userId: string) => {
  // userIdを引数にとって、pushMessageを送信する関数
  const message: TextMessage = {
    type: "text",
    text: "pushメッセージです。",
  };
  await client.pushMessage(userId, message);
};

pushMessage("U95b0ce9db0b4cb73ec243a56e8b78nou");
```

サーバなどは関係ないため、

```bash
npx ts-node pushMessage.ts
```
で特定ユーザにメッセージを送ることができます。

## broadCastMessageを送信する
BotからBotと友達になっている全ユーザに向けてメッセージを送信します。

> broadCastMessage.ts

```typescript
const broadCastMessage = async () => {
  const message: TextMessage = {
    type: "text",
    text: "broadCastメッセージです。全てのユーザに届きます。",
  };
  await client.broadcast( message);
};

broadCastMessage();
```

## リッチメニューを設定する
Botに[リッチメニュー](https://developers.line.biz/ja/docs/messaging-api/using-rich-menus/#using-rich-menus-introduction)を登録することで、ユーザがどのように使えば良いかや
Botが提供する機能について、分かりやすくなります。
ここではリッチメニューの設定方法を示します。

まず、最初にリッチメニューの背景画像を用意します。
リッチメニューの背景画像は以下の要件をみたいしている必要があります。

> 画像フォーマット：JPEGまたはPNG
> 画像の幅サイズ（ピクセル）：800以上、2500以下
> 画像の高さサイズ（ピクセル）：250以上
> 画像のアスペクト比（幅/高さ）：1.45以上
> 最大ファイルサイズ：1MB

※上記のように記載されていますが、私は画像サイズが横2500px,縦1686pxの画像でのみ設定したことがありますが、
 1pxでもズレるとエラーになるため、エラーで進まない場合は、横2500px縦1686pxで画像を作成してみてください。

次に、リッチメニューの画像のどこを押せば何を行うなどの情報を作成するのですが、
その作成には、[LINE Bot Designer](https://developers.line.biz/ja/services/bot-designer/)で作成するのが簡単だと思います。
※Designerの話はここでは詳しくやりません。また、別の機会に書きます。割と直感的に使えると思います。

[LINE Bot Designer](https://developers.line.biz/ja/services/bot-designer/)からリッチメニューの設定を行うと
Designerの下の方からJSON形式の情報を取得できます。(やってみたらわかるはず。適当ですみません。)

画像とJSON形式の情報を作成したら以下のスクリプトに入力してください。

> richMenu.ts

```typescript
import fs from 'fs';
import { RichMenu } from '@line/bot-sdk';

// LINE Designerを使用して作成
const richMenu: RichMenu = <JSONをここに貼り付け>

const registRichMenu = async () => {
  const richMenuId = await client.createRichMenu(richMenu);
  await client.setRichMenuImage(
    richMenuId,
    fs.createReadStream('<リッチメニューの背景画像へのパスを入力>')
  );
  await client.setDefaultRichMenu(richMenuId);
};

registRichMenu();
```

```bash
npx ts-node richMenu.ts
```

上記を実行することによってリッチメニューが登録できます。
`client.setDefaultRichMenu`はデフォルトのリッチメニューを設定します。
ユーザ毎にリッチメニューを分けたい場合は、`client.linkRichMenuToUser(<userId>,<richMenuId>)`
で設定することができます。

## アクセストークンの検証(LINEログインとの連携)
LINEログイン(LIFF,LINE ログイン SDK)を使用したアプリケーションとLINEBotを連携させたいという要望もあったりするかなと思います。
同じプロバイダーでLINEログインのアプリとBotを作成すると、同じユーザには同じuserIDが割り振られるので、簡単に連携することが可能です。

LINEでログインを行うアプリケーションでは、ログインが行われるとLINEからアクセストークンが配布されます。
そのアクセストークンをBotが動いているサーバに送ることによって、Botが連携してメッセージを送信することができるようになります。
詳しくは[こちら](https://developers.line.biz/ja/docs/line-login/secure-login-process/#using-access-tokens)をご覧ください。

この時のアクセストークンの検証を方法を記載します。
少し複雑になってしまいました。後で、ちゃんと修正します。

> validation.ts

```typescript
import fetch from 'node-fetch';
import express from 'express';

const LOGIN_APP_ID = <LINEログインアプリケーションのID>

const LINE_AUTH_API = 'https://api.line.me/oauth2/v2.1/';
const LINE_API = 'https://api.line.me/v2/';


interface ValidationSuccess {
  client_id: string;
  expires_in: number;
  scope: string;
}
interface ValidationError {
  error: string;
  error_description: string;
}

function isValidationError(arg: any): arg is ValidationError {
  return arg.error !== undefined;
}
export const accsessTokenValidation = async (
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) => {
  try {
    const token = req.headers.authorization?.replace('Bearer ', '');
    if (typeof token !== 'string') {
      throw new Error('バリデーションエラー');
    }
    const res = await fetch(`${LINE_AUTH_API}verify?access_token=${token}`);

    const data = (await res.json()) as ValidationSuccess | ValidationError;

    if (isValidationError(data)) {
      // バリデーションエラー
      console.log(data.error);
      throw new Error('バリデーションエラー');
    }

    if ( data.client_id === LOGIN_APP_ID) {
      // 認証成功
      try {
        const res = await fetch(`${LINE_API}/profile`, {
          headers: { Authorization: `Bearer ${token}` },
        });
        const profile = (await res.json()) as { userId: string };

        // userId取得後にheaderに記録、後の処理で使用する
        req.headers.userId = profile.userId;

        next();
      } catch (e) {
        next(new Error('lineのユーザ情報の取得に失敗しました。'));
      }
    } else {
      throw new Error('認証エラー');
    }
  } catch (e) {
    next(e);
  }
};

```

accessTokenの検証が終了すると、accsessTokenからprofileを取得して、userIdを知ることで
さまざまな処理ができるようになります。

> index.ts

```typescript
app.post('/api',accsessTokenValidation,(req,res,next) => {
 const userId = req.headers.userId
 const message: TextMessage = {
    type: "text",
    text: "LINEログインと連携されてメッセージが送られました。",
  };
  await client.pushMessage(userId, message);
  res.send("ok");
})
```

middlewareとして導入することで、好きなところでaccsessTokenの検証ができるようになります。
その後の処理でuserIdを使用してBotからメッセージを送っています。