Node.js
NOW
linebot

LINE BOT作ってNowで動かした時のメモ

これは何?

以下の記事を参考にLINE BOTを作ってみたので、手を加えてみたところ・詰まったところなどを記します。

基本的にはこの記事の通りで、少しだけ変えたりしました。
ありがとうございます。楽しく作れました。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest

一番最後にコードをそのまま載せてます。

作ったBOT

  • 天気を答える
    「今日の天気」「明日の天気」「明後日の天気」のワードで、ライブドアの天気予報APIから情報を取得して返してくれる。

  • 気温を答える
    「今日の気温」「明日の気温」「明後日の気温」のワードで、ライブドアの天気予報APIから情報を取得して返してくれる。

  • イメージ
    写真 2019-01-04 23 35 34.png

構成要素

やったこと

だいたいの手順

BOTとトークできるようになるまでは冒頭で引用させていただいた記事のまんまです。
1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest

この通りにやれば、Nowにデプロイできる環境になっていると思います。

手を加えてみたところ・詰まったところ

BOTを1:1のトークだけでなくルーム、グループにも対応させたい

  • 困ったこと
    例えば友達とのトークルームで「今日の天気」と言った場合、BOTはそのトークルームではなく、「発言者に対して1:1で」メッセージを送ってくる。

  • 原因
    BOTはメッセージの「発言者(userId)」を宛先としてリプライのメッセージを送っている。

  • 対応
    発信元がユーザーの場合はユーザー、トークルームの場合はトークルーム、グループの場合はグループにリプライするようにした。
    具体的にはリクエストの中のsource.type.以下に発信元を表す情報が入っているので、こういう感じで送り返すべき相手を判別することにした。

server.jsより抜粋
  let tempId = ''
  if (event.source.type === 'user') {
    tempId = event.source.userId;
  } else if(event.source.type === 'room') {
    tempId = event.source.roomId;
  } else if(event.source.type === 'group'){
    tempId = event.source.groupId;
  }

evene.source.type === userならuserIdが、roomならroomIdが、groupならgroupIdが設定されてくる。

天気予報APIから返ってきたJSONでnullが入ってる時がある

  • 困ったこと
    「今日の気温」は正しく返ってきても、「明日の気温」が返ってこなかったりする。(日による)

  • 原因
    APIから返却されるJSONの気温データがnullだったりすることがある。
    API(Weather Hacks > お天気Webサービス)の下の方にJSONのサンプルを載せてもらえているのですが、forecasts[0].temperature.maxは設定されているのに対し、forecasts[0].temperature.minnullになっています。
    更なる原因はともかく、気温は場合によってはデータが無い時があるようです。その時は当然、欲しかったcelsiusは取得できません。

  • 対応
    素直にnullチェックした。(もっと楽にできそうですが…)

server.jsより抜粋
  if(item.forecasts[2].temperature.max == null){
    client.pushMessage(tempId, {
      type: 'text',
      text: "最高気温のデータが無いみたいです",
    });
  } else {
    client.pushMessage(tempId, {
      type: 'text',
      text: "最高" + item.forecasts[2].temperature.max.celsius + "度",
    });
  }

Nowでnowした後の公開URLを固定する

  • 困ったこと
    Nowではデプロイ(nowコマンド)する度に一意の公開URLが生成されるので、LINE Developer側で設定し直すのが面倒。

  • 対応
    now.jsonserver.jsと同じ階層に作成し、aliasを設定し、nowでデプロイした後にnow aliasすれば、公開URLを一意にできる。

キャプチャ.PNG
上の画像ではmatch3-bot2-pdaxurecfi.now.shがデプロイ先だが、now aliashttps://match3-bot2.now.shを公開URLとして使えるようになる。
LINE側にはこのURLを設定してやれば、後は修正してデプロイしてもURLは変更しなくてもOKになる。
キャプチャ.PNG

所感

  • LINE Developer
    簡単に作れるので、もう一つくらいBOT作ってみたい。

  • Now
    デプロイ早くて使いやすい。フリープランでも十分いろいろできそう。VSCodeからコマンドでサクッと済むので良いです。

コード

最後に、全くリファクタリングしてませんがコードをそのまま載せておきます。

  • now.json
  • package.json
  • server.js

now.json

now.json
{
    "version": 1,
    "alias": "match3-bot2"
}

package.json

package.json
{
  "name": "match3-bot2",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@line/bot-sdk": "^6.4.0",
    "axios": "^0.18.0",
    "express": "^4.16.4"
  }
}

server.js

server.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
const PORT = process.env.PORT || 3000;

const config = {
  channelSecret: 'ここには自身のmassaging APIのものを入れる',
  channelAccessToken: 'ここには自身のmassaging APIのものを入れる'
};

const app = express();

app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);
    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let tempId = ''
  if (event.source.type === 'user') {
    tempId = event.source.userId;
  } else if(event.source.type === 'room') {
    tempId = event.source.roomId;
  } else if(event.source.type === 'group'){
    tempId = event.source.groupId;
  }

  let mes = ''
  if(event.message.text === '今日の天気'){
    mes = '待っててね';
    getTodayForecast(tempId);
  }else if(event.message.text === '明日の天気'){
    mes = '待っててね';
    getTommorowForecast(tempId);
  }else if(event.message.text === '明後日の天気'){
    mes = '待っててね';
    getDayAfterTommorowForecast(tempId);
  }else if(event.message.text === '今日の気温'){
    mes = '待っててね';
    getTodayTemperature(tempId);
  }
  else if(event.message.text === '明日の気温'){
    mes = '待っててね';
    getTommorowTemperature(tempId);
  }else if(event.message.text === '明後日の気温'){
    mes = '待っててね';
    getDayAfterTommorowTemperature(tempId);
  }

  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: mes
  });
}

const getTodayForecast = async (tempId) => {
    const res = await axios.get('http://weather.livedoor.com/forecast/webservice/json/v1?city=130010');
    const item = res.data;
    await 
    client.pushMessage(tempId, {
      type: 'text',
      text: item.title,
    });
    client.pushMessage(tempId, {
      type: 'text',
      text: item.forecasts[0].telop,
    });
}

const getTommorowForecast = async (tempId) => {
  const res = await axios.get('http://weather.livedoor.com/forecast/webservice/json/v1?city=130010');
  const item = res.data;
  await 
  client.pushMessage(tempId, {
    type: 'text',
    text: item.title,
  });
  client.pushMessage(tempId, {
    type: 'text',
    text: item.forecasts[1].telop,
  });
}

const getDayAfterTommorowForecast = async (tempId) => {
  const res = await axios.get('http://weather.livedoor.com/forecast/webservice/json/v1?city=130010');
  const item = res.data;
  await 
  client.pushMessage(tempId, {
    type: 'text',
    text: item.title,
  });
  client.pushMessage(tempId, {
    type: 'text',
    text: item.forecasts[2].telop,
  });
}

const getTodayTemperature = async (tempId) => {
  const res = await axios.get('http://weather.livedoor.com/forecast/webservice/json/v1?city=130010');
  const item = res.data;

  if(item.forecasts[0].temperature.max == null){
    client.pushMessage(tempId, {
      type: 'text',
      text: "最高気温のデータが無いみたいです",
    });
  } else {
    client.pushMessage(tempId, {
      type: 'text',
      text: "最高" + item.forecasts[0].temperature.max.celsius + "度",
    });
  }

  if(item.forecasts[0].temperature.min == null){
    client.pushMessage(tempId, {
      type: 'text',
      text: "最低気温のデータが無いみたいです",
    });
  } else {
    client.pushMessage(tempId, {
      type: 'text',
      text: "最低" + item.forecasts[0].temperature.min.celsius + "度",
    });
  }
}

const getTommorowTemperature = async (tempId) => {
  const res = await axios.get('http://weather.livedoor.com/forecast/webservice/json/v1?city=130010');
  const item = res.data;

  if(item.forecasts[1].temperature.max == null){
    client.pushMessage(tempId, {
      type: 'text',
      text: "最高気温のデータが無いみたいです",
    });
  } else {
    client.pushMessage(tempId, {
      type: 'text',
      text: "最高" + item.forecasts[1].temperature.max.celsius + "度",
    });
  }

  if(item.forecasts[1].temperature.min == null){
    client.pushMessage(tempId, {
      type: 'text',
      text: "最低気温のデータが無いみたいです",
    });
  } else {
    client.pushMessage(tempId, {
      type: 'text',
      text: "最低" + item.forecasts[1].temperature.min.celsius + "度",
    });
  }
}

const getDayAfterTommorowTemperature = async (tempId) => {
  const res = await axios.get('http://weather.livedoor.com/forecast/webservice/json/v1?city=130010');
  const item = res.data;

  if(item.forecasts[2].temperature.max == null){
    client.pushMessage(tempId, {
      type: 'text',
      text: "最高気温のデータが無いみたいです",
    });
  } else {
    client.pushMessage(tempId, {
      type: 'text',
      text: "最高" + item.forecasts[2].temperature.max.celsius + "度",
    });
  }

  if(item.forecasts[2].temperature.min == null){
    client.pushMessage(tempId, {
      type: 'text',
      text: "最低気温のデータが無いみたいです",
    });
  } else {
    client.pushMessage(tempId, {
      type: 'text',
      text: "最低" + item.forecasts[2].temperature.min.celsius + "度",
    });
  }
}

app.listen(PORT);
console.log(`Server running at ${PORT}`);