Help us understand the problem. What is going on with this article?

LINE BOTをHeroku + Node.jsでやるまで

More than 1 year has passed since last update.

この投稿は LINEBot&Clova Advent Calendar 2018の16日目の投稿です。お邪魔します。

この記事でやること

image.png
上のようなLINE BOTを動かすまでのステップを説明していきます。
これからLINE BOTをやってみようかな、という人向けの記事です。(JavaScript/Node.jsをちょっと知ってる前提ですが、頑張れば知らなくてもできるかも。)
手順解説なので長くなってしまいましたが、沿って行けばLINE BOTを動かすことができるはずです。
冬休みも近いので、ぜひやってみましょう!

ここで使う道具は以下の通りです。

  • Windows 10 PC
  • コマンドプロンプト
  • LINEアカウント(プライベートのでOK)
  • Heroku
    • PaaSです。linebotからのメッセージを処理して返事するAPIサーバとして利用します。Herokuである必要はありません。私が選んだ理由は公式でガイドがあり、自身でAPIサーバ建てたことないためですが、要はRest APIができれば何でもよかったです。Herokuとlinebotはちゃんと疎結合です。でもHerokuはサーバの面倒ごとを全部引き受けてくれるので楽です(特にgit pushすれば勝手にデプロイされるのが楽)。自由にやりたい人には逆に苦痛かも。
  • Node.js
    • サーバサイドでJavaScriptを動かすプログラムです。
  • line-bot-sdk-nodejs
    • LINEのボットサーバ開発で必要なことをラッピングしてくれているものです。requireして使います。以前はこれを使わずにやっていたのですが、今回記事を書くにあたって使ってみるとすごい楽になった!おすすめです。

手順

  1. 道具を準備
  2. LINE@アカウントの作成
  3. ボットサーバの開発

構成図

公式より。

image.png

上図の[Messaging Servers]というところを「ボットサーバ」として、Heroku+Node.jsで構築していきます。

[手順1]道具を準備

道具を準備していきます。

Windows 10 PC

買います

コマンドプロンプト

Windowsボタンを押してcmdと入力すると検索結果にコマンドプロンプトが出てきます。

LINEアカウント

もってますよね?もってなければこちらから!友達とアカウント交換しましょう!

Node.jsとHerokuの準備

以下の記事に沿って作成してください。Rest APIでJSONを返すまでを解説しています。
裸の窓使いがHerokuにNodeでRestAPIで「こんにちは」するまで

[手順2]LINE@アカウントの作成

優しいくわかりやすい人の記事を引用いたします。
LINE BOTの作り方を世界一わかりやすく解説(1)【アカウント準備編】

[手順3]ボットサーバの開発

ボットサーバとして使うサーバは、Node.jsとHerokuの準備で準備したHerokuのインスタンスを使用します。
これをボットサーバとして機能するように修正していきます。
必要なことは、

  • LINE Developersからチャネルの開設してChannel Secretとアクセストークンを取得
  • Heroku上のREST APIをLINE BOTのWebhookに反応できるように修正
  • LINE DevelopersでREST APIのURLをWebhookとして設定

です。Webhookとは、LINEプラットフォームからボットサーバへ送信されるリクエストとそれに対するレスポンスをさします。ボットサーバのURLをWebhook URLとしてLINE Developersで設定することで、接続が可能となります。

LINE Developersからチャネルの開設してChannel Secretとアクセストークンを取得

[チャネル]がいわゆる[友達]にあたるものです。そのチャネルからデータの送受信をしますが、その時に以下の2つの情報利用が必要になります。

  • 当該チャネルからの通信であることを証明するための[Channel Secret]
  • メッセージ送受信するためのトークンとして利用する[アクセストークン]

ここではチャネルを開設し、上記2つをを取得します。

  1. LINE Developersのサイトから、[プロダクト] - [Messaging API] - [今すぐ始めよう]に進んでください image.png
  2. [プロバイダ]が出ていると思うので、選択して[次のページ]を押してください。なければ、[新規プロバイダー作成]でよいです(この場合プロバイダ名を設定することになります。任意の名前でOKです)。 image.png
  3. アプリ名やアイコンを決定していきます。ここで[プラン]選択に[Developer Trial]と[フリー]が出ると思います。[Developer Trial]のほうはpush通知ができますが友達が50人までに制限されます。[フリー]はpush通知はできませんが友達数は無制限です。今回はpush通知はしないので、ここでは[フリー]を選択します。練習なので、業種なんかは適当でよいです。確認ボタンを押すと確認事項のダイアログが出てくるので、同意します。 image.png
  4. 確認画面にくるので、問題なければ[作成]を押します。 image.png
  5. 少し待つとチャネルが作成されます。 image.png
  6. チャネルをクリックして[チャネル基本設定]タブ - [基本情報] - [Channel Secret]を確認します。これを後ほどHeroku側で使い、発行されたリクエストがこのチャネルからのものかを検証するために使います。(以下のChannel Secret配下黒塗りの部分) image.png
  7. 同じく[チャネル基本設定]タブ内の、 [メッセージ送受信設定] - [アクセストークン(ロングターム)] - [再発行]を押してトークンを取得します。(初めてでも[再発行]です。)古いアクセストークンの失効時間を設定させるダイアログがでますが、まだ持ってないので0時間でよいです。 image.png
  8. 少しするとアクセストークンが表示されます(以下の黒塗りの部分、結構ながい)。これも後ほどHeroku側で使用していきます。 image.png

Heroku上のREST APIをLINE BOTのWebhookに反応できるように修正

続いてボットサーバを実装していきます。ここがメインの作業です。
Node.jsとHerokuの準備で作成したインスタンスのindex.jsを修正していきます。

まずはREST APIを用意します。Messaging APIはPOSTでリクエストされてくるので、POSTに反応できる口を作ります。ここでは"./hook"をインターフェースとします。

index.js
const express = require('express')
const path = require('path')
const PORT = process.env.PORT || 5000

express()
  .use(express.static(path.join(__dirname, 'public')))
  .set('views', path.join(__dirname, 'views'))
  .set('view engine', 'ejs')
  .get('/', (req, res) => res.render('pages/index'))
  .get('/g/', (req, res) => res.json({method: "こんにちは、getさん"}))
  .post('/p/', (req, res) => res.json({method: "こんにちは、postさん"}))
  .post("/hook/", (req, res) => res.json({ test: "hook" }))// 追加
  .listen(PORT, () => console.log(`Listening on ${ PORT }`))

heroku localコマンドでローカルサーバを立ち上げ、curlコマンドでPOSTしてみます。

コマンドプロンプト
$ curl -X POST localhost:5000/hook
{"test":"hook"}

思った返事です。pushします。

コマンドプロンプト
$ git add .
$ git commit -m "add lineBot"
$ git push heroku master

いろいろわーっと出て問題なければ完了します。
curlで、今度はサーバ側に届くか確認してみましょう。

コマンドプロンプト
$ curl -X POST https://Herokuのアプリ名.herokuapp.com/hook
{"test":"hook"}

ベネ(よし)。
ではこれで「LINE DevelopersでREST APIのURLをWebhookとして設定」をやってみちゃいましょう。(えっ?て思った方もとりあえず)

LINE DevelopersでWebhookを設定

まずは、Webhookを有効にします。[チャネル基本設定]タブ - [メッセージ送受信設定] - [Webhook送信] - [編集]で[利用する]を選択し[更新]を押します。
image.png

次に直下にある[Webhook URL]に、Herokuに用意したREST APIのURLを入れます。
image.png

最後に、[接続確認]ボタンで接続を確認します。直下に「成功しました」と出ればOKです。
image.png
なお、Herokuは無料プランだと最後の動作から30分でスリープする設定になっています。そのため、久しぶりに接続確認を行うとタイムアウトで接続確認に失敗することがあります。その場合は、再度行ってみると成功するかと思います。

ついでに、同じ画面で[LINE@機能の利用]から[自動応答メッセージ]と[友達追加時あいさつ]をオフにしておきましょう。イベントはこちらでハンドリングするので、いらないものは止めておきます。
image.png

また、同じ画面の下にある[LINEアプリへのQRコード]から、自身のスマホで友達登録をしておきましょう。

image.png

さて、これで繋がりました。良かった良かった、完了。とは当然いきません。

試しに追加したチャネルに何か送信してみてください。はい、既読スルーです。LINEプラットフォームに{"test":"hook"}と返しても、LINE BOTはなにも表示してくれません。
ではボットサーバは何をすればよいでしょうか。LINEプラットフォームからは何が送られてきて、こちらは何を返せばよいのか確認し、いよいよ実装を進めていきましょう。

受信する内容

特定のトークに返事をするための識別子として、Messaging APIではreplyTokenを利用することになっています。この情報は、LINE BOTから受信した内容に埋め込まれています。

公式リファレンスの[Webhook]の章を見ると、以下の例があります。

Webhookイベントオブジェクトの例
{
  "destination": "xxxxxxxxxx", 
  "events": [
    {
      "replyToken": "0f3779fba3b349968c5d07db31eab56f",
      "type": "message",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U4af4980629..."
      },
      "message": {
        "id": "325708",
        "type": "text",
        "text": "Hello, world"
      }
    },
    {
      "replyToken": "8cf9239d56244f4197887e939187e19e",
      "type": "follow",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U4af4980629..."
      }
    }
  ]
}

events[n]replyTokenがありました。このトークンをボットの返信に取り込むことで、LINEプラットフォームはトークを特定することができます。また、events[0].message.textには、LINEのトーク上でユーザーが送信した文字内容が書かれています。画像や音声も対応可能で、何が送られてきたかはevents[n].message.typeで判断可能です。
※なお、お気づきかと思いますがeventsは配列となっています。つまり、一時に複数のイベント情報が来る可能性があることを留意しておいてください。この例では、typemessageとともにfollowのものがあります。これはチャネルがフォローされたときに発火されるイベントです。

返信する

さて、前項で「何が送られてくるか」が分かりました。では次に、どうしたら返信できるかを確認します。

またもや公式リファレンスを確認すると、以下のことが必要であることが分かります。

  • 署名を検証する
  • さっと200番を返す
  • 応答メッセージを送る

ではそれぞれ対応していきましょう。なお、これからコードを書いていきますが、実装をアロー関数の右辺にそのまま書いていくとかなりつらいです。とりあえず以下のようにfunctionを切ってその中で実装していきます。

index.js
const express = require('express')
const path = require('path')
const PORT = process.env.PORT || 5000

express()
  .use(express.static(path.join(__dirname, 'public')))
  .set('views', path.join(__dirname, 'views'))
  .set('view engine', 'ejs')
  .get('/', (req, res) => res.render('pages/index'))
  .get('/g/', (req, res) => res.json({method: "こんにちは、getさん"}))
  .post('/p/', (req, res) => res.json({method: "こんにちは、postさん"}))
  .post("/hook/", (req, res) => lineBot(req, res)) // 変更
  .listen(PORT, () => console.log(`Listening on ${ PORT }`))

// 追加
function lineBot(req, res) {
  res.json({ test: "hook" })
}

Herokuの環境変数にトークン情報をセットする

実装に入る前に、返信に必要なトークン情報2つをHerokuの環境変数にセットします。
JavaScript上に書いてももちろん動きますが、秘密の情報なのでセキュリティ上よろしくないですので。

Herokuのリモート上の環境変数は、以下のコマンドで設定できます。1

コマンドプロンプト
$ heroku config:set SECRET_KEY=XXXX --app Heroku上のアプリ名
$ heroku config:set ACCESS_TOKEN=XXXX --app Heroku上のアプリ名

JavaScriptからは、以下の形で取得できます。

const SECRET_KEY = process.env.SECRET_KEY;
const ACCESS_TOKEN= process.env.ACCESS_TOKEN;

変数名はもちろん任意で結構です。このサーバで複数チャネル面倒見るなどの場合は、チャネル名つけたりなんだリで管理するとよいでしょう。

署名を検証する

まずは、ボットサーバへのリクエストが当該LINEチャネルからきたものかどうかを検証します。

署名検証の方法は、公式リファレンスに開発言語ごとに載っています。が、実は自分で実装しなくてもできます。LINEが各言語ごとに提供してくれているline-bot-sdkというライブラリで実現可能です。

Node.js用はこちらです。それでは早速インストールします。

コマンドプロンプト
$ npm install @line/bot-sdk --save

インストールが完了したら、署名検証を実装していきましょう。
実装については公式のBasic Usageを参考にしていきます。どうやらmiddlewareというものを利用するとよいそうです2。もとの実装に反映すると、以下のような形になります。

index.js
const express = require("express");
const path = require("path");
const PORT = process.env.PORT || 5000;
const line = require("@line/bot-sdk"); // 追加
// 追加
const config = {
  channelAccessToken: process.env.ACCESS_TOKEN,
  channelSecret: process.env.SECRET_KEY
};

express()
  .use(express.static(path.join(__dirname, "public")))
  .set("views", path.join(__dirname, "views"))
  .set("view engine", "ejs")
  .get("/", (req, res) => res.render("pages/index"))
  .get("/g/", (req, res) => res.json({ method: "こんにちは、getさん" }))
  .post("/p/", (req, res) => res.json({ method: "こんにちは、postさん" }))
  .post("/hook/", line.middleware(config), (req, res) => lineBot(req, res)) // 変更、middlewareを追加
  .listen(PORT, () => console.log(`Listening on ${PORT}`));

function lineBot(req, res) {
  res.json({ test: "hook" });
  console.log("pass"); // 追加
}

これで、/hookは当該チャネルからのリクエストしか反応しないようになりました。
lineBot()関数が呼ばれているのを確認するためにログをしこみました。
LINE Developersの[Webhook URL]から[接続確認]を行うと、成功することが分かると思います。また、heroku logsを実行すると、コンソールにpassが表示されていることが分かるはずです。
また、コンソールからcurlを投げてみましょう。これは当該チャネルからのリクエストではありませんのではじかれるはずです。

コマンドプロンプト
$ curl -X POST https://Herokuのアプリ名.herokuapp.com/hook
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Internal Server Error</pre>
</body>
</html>

ちゃんとエラーメッセージが返ってきました。heroku logsを確認してもpassが表示されていません。少しさみしい。

さっと200番を返す

疎通できたので早速応答メッセージを送りたいところですが、まず200番だけを返すようにし、それを返した後に応答メッセージを返すようにします。
理由は、「返事の処理に時間がかかる」とか「メッセージが同時多発できた」という場合、すぐに200番を返してあげないとLINEプラットフォーム側にこちらに対するキューが溜まってしまい、後のお客様がいつまでたっても返事を受け取れないからです。
この方の「大量メッセージが来ても安心なLINE BOTサーバのアーキテクチャ」という記事で非常に詳しく解説いただいているので、ご参照ください。とてもためになります。

では200番を返しましょう。

index.js(抜粋)
function lineBot(req, res) {
  res.status(200).end(); // 変更
  console.log("pass");
}

これだけです。heroku logs --tailでリモート環境のログを見ながらLINE Developersで[Webhook URL]の[接続確認]を行えば、成功しつつログにもpassが表示されることが分かると思います。
res.json({ test: "hook" })はいらないので削除しました。

応答メッセージを送る

ではいよいよ応答メッセージを送ります。
署名検証でも利用したline-bot-sdkに、これまた便利なものがあります。Clientというものです。
本来であればURLの指定やヘッダの構築、ボディの構築諸々を行って応答すべきなのですが、Clientがいろいろラップしてくれます。助かるわ~

では、「<ユーザー名>さん、今<受信テキスト>って言いました?」と応えるbotを作ってみましょう。
こんな感じです。

image.png

すごいうっとうしい。

次に、実装は以下の通りです。

index.js
const express = require("express");
const path = require("path");
const PORT = process.env.PORT || 5000;
const line = require("@line/bot-sdk");
const config = {
  channelAccessToken: process.env.ACCESS_TOKEN,
  channelSecret: process.env.SECRET_KEY
};
const client = new line.Client(config); // 追加

express()
  .use(express.static(path.join(__dirname, "public")))
  .set("views", path.join(__dirname, "views"))
  .set("view engine", "ejs")
  .get("/", (req, res) => res.render("pages/index"))
  .get("/g/", (req, res) => res.json({ method: "こんにちは、getさん" }))
  .post("/p/", (req, res) => res.json({ method: "こんにちは、postさん" }))
  .post("/hook/", line.middleware(config), (req, res) => lineBot(req, res))
  .listen(PORT, () => console.log(`Listening on ${PORT}`));

function lineBot(req, res) {
  res.status(200).end();
  // ここから追加
  const events = req.body.events;
  const promises = [];
  for (let i = 0, l = events.length; i < l; i++) {
    const ev = events[i];
    promises.push(
      echoman(ev)
    );
  }
  Promise.all(promises).then(console.log("pass"));
}

// 追加
async function echoman(ev) {
  const pro =  await client.getProfile(ev.source.userId);
  return client.replyMessage(ev.replyToken, {
    type: "text",
    text: `${pro.displayName}さん、今「${ev.message.text}」って言いました?`
  })
}

解説していきます。

大まかな流れとしては、

  • 受信したbodyからeventを解析
  • client.getProfile()を使ってユーザー名を取得
  • client.replyMessage()を使って返事を送信
  • 一通り終わったらログだけ出しておく

です。

受信したbodyからeventを解析

まずreq.bodyですが、すでにline.middlewareがパースしてくれているので普通に使えます。ありがたい。逆に、自前でbody-parser使ったりしてると注意が必要です。以下公式のmiddleware Usageより抜粋です。

You do not need to use body-parser to parse webhook events, as middleware() embeds body-parser and parses them to objects. Please keep in mind that it will not process requests without X-Line-Signature header. If you have a reason to use body-parser for other routes, please do not use it before the LINE middleware. body-parser parses the request body up and the LINE middleware cannot parse it afterwards.

bodyの内容は受信する内容で述べた通りです。
ここではclient.getProfile()で使うevent.source.userIdと、client.replyMessage()で使うevent.message.textを使います。

client.getProfile()を使ってユーザー名を取得

ユーザー情報を取得します。

index.js(echoman()より抜粋)
  const pro =  await client.getProfile(ev.source.userId);

LINEプラットフォームから非同期通信でデータを取得しています。その情報を後続の処理で利用するので、async/awaitを使って同期化します。

client.replyMessage()を使って返事を送信

ここでメッセージを返します。

index.js(echoman()より抜粋)
return client.replyMessage(ev.replyToken, {
    type: "text",
    text: `${pro.displayName}さん、今「${ev.message.text}」って言いました?`
  })

先ほど取得したユーザー情報から名前と、ユーザーが送信したtextを返信bodyに詰め込んで返信を送ります。
client.replyMessageは非同期通信で、Promiseを返します。これをechoman()の呼び出し元で取得して、完了後の処理を行います。

一通り終わったらログだけ出しておく

最後に、処理の完了だけ検知してログを出しておきます。これはついでなので、別にやる必要はないですが。

index.js
  const events = req.body.events;
  const promises = [];
  for (let i = 0, l = events.length; i < l; i++) {
    const ev = events[i];
    promises.push(
      echoman(ev)
    );
  }
  Promise.all(promises).then(console.log("pass"));

echoman()で返却されるPromiseを配列に格納して、全部の完了をPromise.all()で検知し、then()の中でconsole.log()を呼び出してログを表示します。
Promise周りの仕様を確認したい方は、NDMをご参照ください。

さて、長くなりましたがここで本記事で作るものは完成しました。

LINE BOTでもっといろいろ

いろいろやるためのヒントをリンクを交えて(というかメインで)お送りします。

いろいろな返信

LINE Messaging API でできることまとめ【送信編】

文字だけでなく、スタンプや動画音声も送れます。また、テンプレートを使うと見やすいレイアウトでユーザーに送ることもできます。このあたりのレイアウトを、自分で考えずにJSON送れば共通のUIで良しなにやってくれるのがLINE BOTのいいところだと思います。すぐに訴求可能なメッセージを送れる。

いろいろなREST API

世の中にはいろんな情報を取得可能なAPIがあります。
API 面白いとかで調べるといろいろ出てくるかと思います。

フロントエンド制作に華を! 面白いWebAPI 7選
海外・国内の便利なAPI一覧 - API LIST 100+

ほかにもいろいろ出てきます。結構いろいろ妄想すると楽しいですね。
例えば「画像で物知り博士」と題して、ユーザーから画像を受け取り、Google Cloud Visionで画像ラベルを取得 -> Google Translate APIでラベルを日本語に翻訳 -> MediaWiki APIを使ってそれぞれの検索結果を取得 -> カルーセルテンプレートで表示、とか。
考え始めると「じゃああのAPIはないかな!?」とか気になりだして面白くなってきます。アイデアをメモっておいて、まとまった休みに実装してみるとかいいですね。

終わりに

手順に書き下すと長いですが、一度できてしまえばあとはいろいろやりやすいです。LINE BOTではレイアウトに悩むことが少ないことも、継続開発の助けになります。開発とかよく知らない友人に自分のチャネルを紹介すれば、「マジやばくな~い?すごくな~い?」と言われることうけあいです。
まだやったことない人も、ぜひやってみましょう!


  1. ちなみにこの環境変数の設定はリモート側にのみ反映されますが、ローカルと動機させる便利なプラグインもあります。こちら(外部サイト)に分かり易く書いてくれています。 

  2. middlewareのusageに、次のように記載されています。The middleware returned by middleware() parses body and checks signature validation, so you do not need to use validateSignature() directly. 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away