4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

LINEWORKSAdvent Calendar 2019

Day 20

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

Last updated at Posted at 2019-12-19

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

本記事では、LINE WORKS Bot のメッセージ受信 API を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

##メンバーからのメッセージを受信 (message)

05_sendmessage.png

##Bot が複数人トークルームに招待された (join)

13_addpanda.png

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

##メンバーが Bot のいるトークルームに参加した (joined)

10_addpanda.png

##メンバーが Bot のいるトークルームから退室した (left)

09_outpanda.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開発!(前編) の「Developer ConsoleでAPIを使うための設定とBotを登録する
    ※Bot登録の際に指定する Callback URL は、ngrokを利用して取得するとローカルデバッグができるのでとっても便利です。
    (記事:ローカル環境で LINEWORKS Bot を動かす話が大変参考になりました)

  2. LINE WORKS の管理画面で、Developer Console で登録したBotをメンバーが利用できる様に設定します。
    ↓こちらの記事を参考に作業していただければと思います。
    LINE WORKSで初めてのBot開発!(後編) の「Botを公開し利用する

開発環境

  • VS Code:IDE
  • node.js+Express:Botサーバー
  • dotenv:アプリケーションの環境変数定義
  • ngrok:ローカルデバッグ

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

#4. 作ってみる

まずはメインのjs (server.js)

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

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)メソッドです。

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 が複数人のトークルームに招待された
        // ※メンバー1人と Bot のトークルームに他のメンバーを招待したらjoinがコールされる(最初の1回だけ)
        //  招待したメンバーを退会させ、再度他のメンバーを招待するとjoinedがコールされるこれ仕様?
        //  たぶん、メンバー1人と 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

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

Terminal
ngrok http 3000
ngrok2.png ※ ForwardingもとのURLが変わった場合は、Developer Console で Botの Callback URL の変更を必ずしてください。

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

####アクター

  • Bot:ボトやま
  • メンバー1:栗井 (スマートフォンを操作している人)
  • メンバー2:パンダD

####シナリオ1:ボトやまの利用を開始してみる (message)

01_roomlist.png
02_selectbot.png
03_openbot.png
04_welcomebot.png

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

####シナリオ2:栗井からメッセージを送信してみる (message)

05_sendmessage.png

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

####シナリオ3:栗井とパンダDのトークルームにボトやまを招待してみる (join)

11_kaproom.png
12_addmenber.png
02_selectbot.png
13_addpanda.png

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

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

####シナリオ5:栗井とボトやまのトークルームにパンダDを招待してみる (joined)

06_addmenber.png
07_selectpanda.png

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

08_openpanda.png

↓パンダD退室

09_outpanda.png

↓もう一度招待する

10_addpanda.png

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

####シナリオ6:栗井/パンダD/ボトやまのトークルームからパンダDを退室させてみる (left)

09_outpanda.png

想定通りの動き

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

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

#Link

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?