最近、アンタッチャブルが奇跡の復活を遂げましたね。嬉しい限りです。
どうも@shotamako 初めての投稿 & LINEWORKS Advent Calendar 2019 / 20日目の記事です。
本記事では、LINE WORKS Bot のメッセージ受信 API をnode.jsでひと通り触ってみたいと思います。
#0. はじめに
記事の流れになります。
- こんなの作ります
- 環境準備
- 作ってみる
- 動かしてみる
- まとめ
#1. こんなの作ります
LINE WORKS Botのメッセージ受信(callback)には下表のタイプが存在し、そのタイプによって送信するメッセージを切替えるBotを作ります。
callbackタイプ | 説明 |
---|---|
message | メンバーからのメッセージ |
join | Bot が複数人トークルームに招待された |
leave | Bot が複数人トークルームから退室した |
joined | メンバーが Bot のいるトークルームに参加した |
left | メンバーが Bot のいるトークルームから退室した |
postback | postback タイプのメッセージ |
##Botの利用開始 (message)
##メンバーからのメッセージを受信 (message)
##Bot が複数人トークルームに招待された (join)
##Bot が複数人トークルームから退室した (leave)
Botがトークルームから退室したコールバックのため、メッセージを送信することはできません。
##メンバーが Bot のいるトークルームに参加した (joined)
##メンバーが Bot のいるトークルームから退室した (left)
##postback タイプのメッセージ (postback)
postbackは、次回の記事で書きま〜す。
#2. 環境準備
まずは LINE WORKS Bot API の利用準備と開発環境を整えたいと思います。
##LINE WORKS Bot APIの利用準備
-
LINE WORKS の Developer Console で(今回開発する)Botサーバーが LINE WORKS と通信するために必要な接続情報の発行とBotの登録を行います。
↓こちらの記事を参考に作業していただければと思います。
LINE WORKSで初めてのBot開発!(前編) の「Developer ConsoleでAPIを使うための設定とBotを登録する」
※Bot登録の際に指定する Callback URL は、ngrokを利用して取得するとローカルデバッグができるのでとっても便利です。
(記事:ローカル環境で LINEWORKS Bot を動かす話が大変参考になりました) -
LINE WORKS の管理画面で、Developer Console で登録したBotをメンバーが利用できる様に設定します。
↓こちらの記事を参考に作業していただければと思います。
LINE WORKSで初めてのBot開発!(後編) の「Botを公開し利用する」
開発環境
node.jsでいろいろpackageを利用してますが省略します。
#4. 作ってみる
まずはメインのjs (server.js)
server.jsでは、リクエストの受け口、改竄防止や LINE WORKS の Access token を取得するプログラムを書いてます。
(Access token をちゃんと管理してません。近々に対応策を書こうと思います。。。DBが必要になるな〜。。。)
あと、BotMessageServiceクラスのsendメソッドでメッセージ送受信を制御してます。
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)メソッドです。
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の利用準備」で発行した接続情報を設定する。
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 をインストール
npm install
###3. http 3000 で ngrok 起動!
ngrok http 3000
###4. スマフォを手に持って LINE WORKS を動かす
####アクター
- Bot:ボトやま
- メンバー1:栗井 (スマートフォンを操作している人)
- メンバー2:パンダD
####シナリオ1:ボトやまの利用を開始してみる (message)
想定通りのうごきですね。
####シナリオ2:栗井からメッセージを送信してみる (message)
想定通りのうごきですね。
####シナリオ3:栗井とパンダDのトークルームにボトやまを招待してみる (join)
想定通りのうごきですね。
####シナリオ4:栗井/パンダD/ボトやまのトークルームからボトやまを退室させてみる (leave)
Botがトークルームから退室したコールバックのため、メッセージを送信することはできません。
(consoleログが出力されます)
####シナリオ5:栗井とボトやまのトークルームにパンダDを招待してみる (joined)
↓ あれ? Callback タイプが joined だと思いきや、join みたいですね。。。想定と違う。。。(なので、一度パンダDに退室してもらう)
↓パンダD退室
↓もう一度招待する
↑これが想定通りの動き(なんでだろう。。。)
####シナリオ6:栗井/パンダD/ボトやまのトークルームからパンダDを退室させてみる (left)
想定通りの動き
#6. まとめ
LINE WORKS Bot APIのメッセージ受信部分の動作をひと通り確認できました。(一部気になるところがありますが。。。)
今回作成たコードは GitHub の line-works-bot01-node の tag:v1.0 で公開してま〜す。(issueがあればお知らせください。修正します。)
次回クリスマス記事もがんばります!メッセージ送信API!
#Link