LINE WORKS Bot APIをひと通り触ってみる（node.js）#1


最近、アンタッチャブルが奇跡の復活を遂げましたね。嬉しい限りです。:grin:
どうも[@shotamako](https://qiita.com/shotamaco) 初めての投稿 & LINEWORKS Advent Calendar 2019 / 20日目の記事です。

本記事では、LINE WORKS Bot の[メッセージ受信 API](https://developers.worksmobile.com/jp/document/1005009?lang=ja) をnode.jsでひと通り触ってみたいと思います。

#0. はじめに
記事の流れになります。

1. こんなの作ります
2. 環境準備
3. 作ってみる
4. 動かしてみる
5. まとめ

#1. こんなの作ります
LINE WORKS Botのメッセージ受信（callback）には下表のタイプが存在し、そのタイプによって送信するメッセージを切替えるBotを作ります。

| callbackタイプ | 説明 |
|:--|:--|
| message | メンバーからのメッセージ |
| join | Bot が複数人トークルームに招待された |
| leave | Bot が複数人トークルームから退室した |
| joined | メンバーが Bot のいるトークルームに参加した |
| left | メンバーが Bot のいるトークルームから退室した |
| postback | postback タイプのメッセージ |

##Botの利用開始 （message）
![04_welcomebot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/e8ed08af-7520-ab47-fa58-851d43debd46.png)|
|:-:|

##メンバーからのメッセージを受信 （message）
![05_sendmessage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/78cb7175-9add-a6cc-24eb-c817bf3c2a27.png)|
|:-:|

##Bot が複数人トークルームに招待された （join）
![13_addpanda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/7776f803-977d-2da3-2e2c-787842799561.png)|
|:-:|

##Bot が複数人トークルームから退室した （leave）
Botがトークルームから退室したコールバックのため、メッセージを送信することはできません。

##メンバーが Bot のいるトークルームに参加した （joined）
![10_addpanda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/aeb10a42-785e-0547-9d61-01181caf9879.png)|
|:-:|

##メンバーが Bot のいるトークルームから退室した （left）
![09_outpanda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/797ba6b8-9d4c-71e5-2f19-262848c9df51.png)|
|:-:|

##postback タイプのメッセージ (postback)
postbackは、次回の記事で書きま〜す。

#2. 環境準備

まずは LINE WORKS Bot API の利用準備と開発環境を整えたいと思います。

##LINE WORKS Bot APIの利用準備

1. LINE WORKS の Developer Console で（今回開発する）Botサーバーが LINE WORKS と通信するために必要な接続情報の発行とBotの登録を行います。
↓こちらの記事を参考に作業していただければと思います。
[LINE WORKSで初めてのBot開発！(前編)](https://qiita.com/tokotan/items/f615f4a62219d655436f) の「[Developer ConsoleでAPIを使うための設定とBotを登録する](https://qiita.com/tokotan/items/f615f4a62219d655436f#developer-console%E3%81%A7api%E3%82%92%E4%BD%BF%E3%81%86%E3%81%9F%E3%82%81%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%81%A8bot%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B)」
※Bot登録の際に指定する Callback URL は、[ngrok](https://ngrok.com/)を利用して取得するとローカルデバッグができるのでとっても便利です。
（記事：[ローカル環境で LINEWORKS Bot を動かす話](https://qiita.com/kunihiros/items/c9a5340f4512149e580d)が大変参考になりました）

2. LINE WORKS の管理画面で、Developer Console で登録したBotをメンバーが利用できる様に設定します。
↓こちらの記事を参考に作業していただければと思います。
[LINE WORKSで初めてのBot開発！(後編)](https://qiita.com/tokotan/items/976d35ca56132e0bb5c1) の「[Botを公開し利用する](https://qiita.com/tokotan/items/976d35ca56132e0bb5c1#bot%E3%82%92%E5%85%AC%E9%96%8B%E3%81%97%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B)」

## 開発環境
 * VS Code：IDE
 * node.js+Express：Botサーバー
 * [dotenv](https://www.npmjs.com/package/dotenv)：アプリケーションの環境変数定義
 * [ngrok](https://ngrok.com/)：ローカルデバッグ

node.jsでいろいろpackageを利用してますが省略します。

#4. 作ってみる

## まずはメインのjs (server.js)
server.jsでは、リクエストの受け口、改竄防止や LINE WORKS の Access token を取得するプログラムを書いてます。
（Access token をちゃんと管理してません。近々に対応策を書こうと思います。。。DBが必要になるな〜。。。）
あと、BotMessageServiceクラスのsendメソッドでメッセージ送受信を制御してます。

```JavaScript:server.js

const express = require('express');
const app = express();
require('dotenv').config();
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const request = require('request');
const BotMessageService = require('./BotMessageService');

var port = process.env.PORT || 3000
app.listen(port, function() {
    console.log('To view your app, open this link in your browser: http://localhost:' + port);
});

app.use(express.json({verify:(req, res, buf, encoding) => {
  // メッセージの改ざん防止
  const data = crypto.createHmac('sha256', process.env.API_ID).update(buf).digest('base64');
  const signature = req.headers['x-works-signature'];

  if (data !== signature) {
    throw 'NOT_MATCHED signature';
  }
}}));

/* 
* 疎通確認API
*/
app.get('/', function (req, res) {
  res.send('起動してます！');
});

/**
 * LINE WORKS からのメッセージを受信するAPI
 */
app.post('/callback', async function (req, res, next) {
  res.sendStatus(200);
  try {
    const serverToken = await getServerTokenFromLineWorks();
    const botMessageService = new BotMessageService(serverToken);
    await botMessageService.send(req.body);
  } catch (error) {
    return next(error);
  }
});

/** 
 * JWTを作成します。
 * @return {string} JWT
 */
function createJWT() {
  const iss = process.env.SERVER_ID;
  const iat = Math.floor(Date.now() / 1000);
  const exp = iat + 60;
  const cert = process.env.PRIVATE_KEY;

  return new Promise((resolve, reject) => {
    jwt.sign({ iss: iss, iat: iat, exp: exp }, cert, { algorithm: 'RS256' }, (error, jwtData) => {
      if (error) {
        console.log('createJWT error')
        reject(error);
      } else {
        resolve(jwtData);
      }
    });
  });
}

/**
 * LINE WORKS から Serverトークンを取得します。
 * @return {string} Serverトークン
 */
async function getServerTokenFromLineWorks() {
  const jwtData = await createJWT();
  // 注意:
  // このサンプルでは有効期限1時間のServerトークンをリクエストが来るたびに LINE WORKS から取得しています。
  // 本番稼働時は、取得したServerトークンを NoSQL データベース等に保持し、
  // 有効期限が過ぎた場合にのみ、再度 LINE WORKS から取得するように実装してください。
  const postdata = {
    url: `https://authapi.worksmobile.com/b/${process.env.API_ID}/server/token`,
    headers : {
      'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    },
    form: {
      grant_type: encodeURIComponent('urn:ietf:params:oauth:grant-type:jwt-bearer'),
      assertion: jwtData
    }
  };
  return new Promise((resolve, reject) => {
    // LINE WORKS から Serverトークンを取得リクエスト
    request.post(postdata, (error, response, body) => {
      if (error) {
        console.log('getServerTokenFromLineWorks error');
        reject(error);
      } else {
        resolve(JSON.parse(body).access_token);
      }
    });
  });
}
```

##メッセージの送受信制御 (BotMessageService.js)
ベタ書きですが、BotMessageServiceクラスの_getResponse(callbackEvent)メソッドでメッセージの受信内容を解釈して、送信するメッセージ内容を決定してます。
実際 LINE WORKSにメッセージを送信しているところは、send(callbackEvent)メソッドです。

```JavaScript:BotMessageService.js
const request = require('request');

const CALL_BACK_TYPE = {
  message : 'message',
  join : 'join',
  leave : 'leave',
  joined : 'joined',
  left : 'left',
  postback : 'postback',
};

/**
 * BotMessageServiceクラス
 */
module.exports = class BotMessageService {

  /**
   * BotMessageServiceを初期化します。
   * @param {string} serverToken Serverトークン
   */
  constructor (serverToken) {
    this._serverToken = serverToken;
  }

  /**
   * LINE WORKS にBotメッセージを送信します。
   * @param {object} callbackEvent リクエストのコールバックイベント
   */
  async send(callbackEvent) {
    let res = this._getResponse(callbackEvent);
    if (!res) {
      return;
    }
    return new Promise((resolve, reject) => {
      // LINE WORKS にメッセージを送信するリクエスト
      request.post(this._createMessage(res), (error, response, body) => {
          if (error) {
            console.log('BotService.send error')
            console.log(error);
          }
          console.log(body);
          // 揉み消してます！
          resolve();
      });
    });
  }

  /**
   * LINE WORKS に送信するBotメッセージを作成して返します。
   * @param {object} res レスポンスデータ
   */
  _createMessage(res) {
    return {
      url: `https://apis.worksmobile.com/${process.env.API_ID}/message/sendMessage/v2`,
      headers: {
        'Content-Type': 'application/json;charset=UTF-8',
        consumerKey: process.env.CONSUMER_KEY,
        Authorization: `Bearer ${this._serverToken}`
      },
      json: res
    };
  }

  /**
   * メンバーIDを連結して返します。
   * @param {Array} memberList メンバーリスト
   */
  _buildMember(memberList) {
    let result = '';
    if (memberList) {
      memberList.forEach(m => {
        if (result.length > 0) {
          result += ',';
        }
        result += m;
      });
    }
    return result;
  }
  
  /**
   * Bot実装部
   * @param {object} callbackEvent リクエストのコールバックイベント
   * @return {string} レスポンスメッセージ
   */
  _getResponse(callbackEvent) {
    console.log(callbackEvent);

    let res = {
      botNo : Number(process.env.BOT_NO),
    };
    if (callbackEvent.source.roomId) {
      // 受信したデータにトークルームIDがある場合は、送信先にも同じトークルームIDを指定します。
      res.roomId = callbackEvent.source.roomId;
    } else {
      // トークルームIDがない場合はBotとユーザーとの1:1のチャットです。
      res.accountId = callbackEvent.source.accountId;
    }

    switch (callbackEvent.type) {
      case CALL_BACK_TYPE.message:
        // メンバーからのメッセージ
        if (callbackEvent.content.postback == 'start') {
          // メンバーと Bot との初回トークを開始する画面で「利用開始」を押すと、自動的に「利用開始」というメッセージがコールされる
          console.log(`start`);
          res.content = { type: 'text', text: 'ト〜クルームに〜〜。ボトやまが〜くる〜！' };
          return res;
        }

        console.log(CALL_BACK_TYPE.message);
        res.content = { type: 'text', text: 'からの〜〜〜。' };
        break;

      case CALL_BACK_TYPE.join:
        // Bot が複数人トークルームに招待された
        // このイベントがコールされるタイミング
        //  ・API を使って Bot がトークルームを生成した
        //  ・メンバーが Bot を含むトークルームを作成した
        //  ・Bot が複数人のトークルームに招待された
        // ※メンバー１人と Bot のトークルームに他のメンバーを招待したらjoinがコールされる（最初の１回だけ）
        //  招待したメンバーを退会させ、再度他のメンバーを招待するとjoinedがコールされるこれ仕様？
        //  たぶん、メンバー１人と Botの場合、トークルームIDが払い出されてないことが原因だろう。。。
        console.log(CALL_BACK_TYPE.join);
        res.content = { type: 'text', text: 'うぃーん!' };
        break;

      case CALL_BACK_TYPE.leave:
        // Bot が複数人トークルームから退室した
        // このイベントがコールされるタイミング
        //  ・API を使って Bot を退室させた
        //  ・メンバーが Bot をトークルームから退室させた
        //  ・何らかの理由で複数人のトークルームが解散した
        console.log(CALL_BACK_TYPE.leave);
        break;

      case CALL_BACK_TYPE.joined: {
        // メンバーが Bot のいるトークルームに参加した
        // このイベントがコールされるタイミング
        //  ・Bot がトークルームを生成した
        //  ・Bot が他のメンバーをトークルームに招待した
        //  ・トークルームにいるメンバーが他のメンバーを招待した
        console.log(CALL_BACK_TYPE.joined);
        res.content = { type: 'text', text: `${this._buildMember(callbackEvent.memberList)} いらっしゃいませ〜そのせつは〜` };
        break;
      }

      case CALL_BACK_TYPE.left: {
        // メンバーが Bot のいるトークルームから退室した
        // このイベントがコールされるタイミング
        //  ・Bot が属するトークルームでメンバーが自ら退室した、もしくは退室させられた
        //  ・何らかの理由でトークルームが解散した
        console.log(CALL_BACK_TYPE.left);
        res.content = { type: 'text', text: `${this._buildMember(callbackEvent.memberList)} そうなります？` };
        break;
      }

      case CALL_BACK_TYPE.postback:
        // postback タイプのメッセージ
        // このイベントがコールされるタイミング
        //  ・メッセージ送信(Carousel)
        //  ・メッセージ送信(Image Carousel)
        //  ・トークリッチメニュー
        // ※次回の記事で作り込みます。
        console.log(CALL_BACK_TYPE.postback);
        break;

      default:
        console.log('知らないコールバックですね。。。');
        return null;
    }

    return res;
  }
}
```

##環境変数 (.env)
.env.sample ファイルを .env にへんこうする
「LINE WORKS Bot APIの利用準備」で発行した接続情報を設定する。

```:.env
API_ID="API ID"
CONSUMER_KEY="Consumer key"
SERVER_ID="Server ID"
PRIVATE_KEY="認証キー"
BOT_NO="Bot No"
```

#5. 動かしてみる

##いざデバッグ開始！

###1. VS Code のターミナルでプログラムが使用している node.js の package をインストール

```:VsCodeTerminal
npm install
```

###2. デバッグボタン(F5)クリック！
![vscode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/c116999e-d2ac-2057-6055-ccaf6b7d82b3.png)

###3. http 3000 で ngrok 起動！

```:Terminal
ngrok http 3000
```

<img width="644" alt="ngrok2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/af241803-5c74-ac50-edc0-8b1a88732325.png">
※ ForwardingもとのURLが変わった場合は、Developer Console で Botの Callback URL の変更を必ずしてください。

###4. スマフォを手に持って LINE WORKS を動かす

####アクター
* Bot：ボトやま
* メンバー１：栗井　（スマートフォンを操作している人）
* メンバー２：パンダD

####シナリオ１：ボトやまの利用を開始してみる　（message）
![01_roomlist.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/2603a98f-f36a-5f47-5e97-0edfbb079499.png)|
|:-:|

![02_selectbot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/7009551f-4531-f68b-1ed7-bda83a7e8b7e.png)|
|:-:|

![03_openbot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/8d66e50b-0214-6cc8-3f16-6f826a550126.png)|
|:-:|

![04_welcomebot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/a1c3f70a-a104-e63a-a1d9-c2ce1db84666.png)|
|:-:|

想定通りのうごきですね。

####シナリオ２：栗井からメッセージを送信してみる　（message）
![05_sendmessage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/e14bde67-992c-de03-64ff-fe49a1aa2a40.png)|
|:-:|

想定通りのうごきですね。

####シナリオ３：栗井とパンダDのトークルームにボトやまを招待してみる　（join）
![11_kaproom.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/95a81ede-c149-e675-edfe-19bdcee3d1ef.png)|
|:-:|

![12_addmenber.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/b16c5dd4-0876-3e2c-47b4-abcb1d74acc3.png)|
|:-:|

![02_selectbot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/778e4d3f-6b0b-186e-8f25-433710ff0e27.png)|
|:-:|

![13_addpanda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/7e9b12a7-cffe-80bb-dc8d-13f758f1ae2b.png)|
|:-:|

想定通りのうごきですね。

####シナリオ４：栗井／パンダD／ボトやまのトークルームからボトやまを退室させてみる　（leave）
Botがトークルームから退室したコールバックのため、メッセージを送信することはできません。
（consoleログが出力されます）

####シナリオ５：栗井とボトやまのトークルームにパンダDを招待してみる　（joined）
![06_addmenber.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/6be8e00e-c6d5-00a9-849a-c7a5c17342d2.png)|
|:-:|

![07_selectpanda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/608b01ca-6d0a-01dc-6f87-d5f462ae531a.png)|
|:-:|

↓ あれ？ Callback タイプが joined だと思いきや、join みたいですね。。。想定と違う。。。（なので、一度パンダDに退室してもらう）

![08_openpanda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/fb117ba5-f04b-f647-4f8d-f9dee41a5617.png)|
|:-:|

↓パンダD退室

![09_outpanda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/34b570a0-9a82-31e2-7bd4-1ef469157831.png)|
|:-:|

↓もう一度招待する

![10_addpanda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/4029ecf8-64a4-4702-c755-baf491bb4382.png)|
|:-:|

↑これが想定通りの動き（なんでだろう。。。）

####シナリオ６：栗井／パンダD／ボトやまのトークルームからパンダDを退室させてみる　（left）
![09_outpanda.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528703/34b570a0-9a82-31e2-7bd4-1ef469157831.png)|
|:-:|

想定通りの動き

#6. まとめ
LINE WORKS Bot APIのメッセージ受信部分の動作をひと通り確認できました。（一部気になるところがありますが。。。）
今回作成たコードは GitHub の [line-works-bot01-node](https://github.com/shotamaco/line-works-bot01-node) の tag:v1.0 で公開してま〜す。（issueがあればお知らせください。修正します。）

次回クリスマス記事もがんばります！メッセージ送信API！

#Link
  * [LINE WORKS Developers](https://developers.worksmobile.com/)
  * [LINE WORKS ガイド（全般）](https://guide.worksmobile.com/)
  * [LINE WORKS ガイド（管理者画面）](https://guide.worksmobile.com/admin/)
  * [LINE WORKS コミュニティ](https://community.worksmobile.com/)
  * [LINE WORKS ヘルプセンター](https://help.worksmobile.com/)
  * [LINE WORKS Blog](https://line.worksmobile.com/jp/blog/)
  * [さわってわかったLINE WORKS](https://ascii.jp/elem/000/001/955/1955720/)