無料APIを使ったLINE Bot


#はじめに

無料で使えるAPIを使ったLINE Botを作ってみました。
普段の情報取得操作をちょこっとだけ便利に。

各APIの紹介 や 使った感想 などを綴ります。

#目次

使ったAPIの紹介と感想
画面イメージ
サンプルコード

#使ったAPIの紹介と感想

* livedoor提供のお天気情報API
   - 概要：お天気情報を取得するAPI
   - API仕様：http://weather.livedoor.com/weather_hacks/webservice
   - サンプルコード：以下 checkWeatherForecast を参照
   - 感想：気軽に確認できるので便利。<BR>
　　　降水確率などの細かい情報は取得できないのでURL表示して飛べるようにするのが良い。
<BR>
<BR>
* Wikiソフトウェア「[MediaWiki](https://www.mediawiki.org/wiki/Manual:What_is_MediaWiki%3F/ja)」のAPI
   - 概要：Wikipediaの情報を取得するAPI
   - API仕様：https://www.mediawiki.org/wiki/API:Main_page/ja
   - サンプルコード：以下 lookUpWords を参照
<BR>
<BR>
* docomo提供のAI系API
   - 概要：言語解析、音声認識、画像認識、文字認識などができるAPI
   - API仕様：https://dev.smt.docomo.ne.jp/?p=docs.common.index<BR>
　　　　※今回使ったAPIは、[「固有表現抽出」](https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=language_analysis&p_name=api_2#tag01)のみ<BR>
　　　　※API利用には、docomo Developer supportのアカウント登録が必要
   - サンプルコード：以下 analysisSentence を参照
   - 感想：今回対応した使い方は、日々の情報取得を便利にするといったものではないけれど<BR>
　　　「どの程度解析できるようになっているのかなー」という個人的興味を満たしてくれるので<BR>
　　　　他のAPIも使ってみたいところ。
<BR>
<BR>
* ぐるなび提供の飲食店情報API
   - 概要：ぐるなびの飲食店情報を取得するAPI
   - API仕様：https://api.gnavi.co.jp/api/manual/<BR>
　　　　※今回使ったAPIは、[「レストラン検索API」](https://api.gnavi.co.jp/api/manual/restsearch/)<BR>
　　　　※API利用には、ぐるなびWEBサービスでのアカウント登録が必要
   - サンプルコード：以下 gurunaviSearch を参照
   - 感想：「よく検索する条件の指定」と「見たい検索結果情報のみ表示」ができるAPIになっているので<BR>
　　　　個人的にはなかなか使い勝手がよい。<BR>


#画面イメージ

livedoor提供のお天気情報API
![スクリーンショット_天気.png](https://qiita-image-store.s3.amazonaws.com/0/266162/340e2844-bfa2-335c-150d-92d9b7c2cc4e.png)

Wikiソフトウェア「MediaWiki」
![スクリーンショット_Wiki検索.png](https://qiita-image-store.s3.amazonaws.com/0/266162/8ceb9e3b-3486-f461-24d6-9688fd9bb792.png)

docomo提供のAI系API
![スクリーンショット_言語解析.png](https://qiita-image-store.s3.amazonaws.com/0/266162/99aed072-7f7a-e971-2339-1dc7fbcd00eb.png)

ぐるなび提供の飲食店情報API
![スクリーンショット_ぐるなび検索.png](https://qiita-image-store.s3.amazonaws.com/0/266162/41d28b19-50a6-4338-bd82-696e0e978e9a.png)

ヘルプ
![スクリーンショット_ヘルプ.png](https://qiita-image-store.s3.amazonaws.com/0/266162/858c5eaa-41ee-b5e0-619a-1fea07951a59.png)

その他：おうむ返し
![スクリーンショット_おうむ返し.png](https://qiita-image-store.s3.amazonaws.com/0/266162/95454ac8-79fa-6e1e-959d-e9ccd116041b.png)


#サンプルコード

※ 必要な開発環境
　1. LINE Botを作ってあること
　2. node.js環境があること
　→環境が整っていない場合は、@n0bisuke さんの[「1時間でLINE BOTを作るハンズオン」](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)が参考になります。
※ コード上の「Confidential information」部分については、自分の環境に合わせて修正してください。

```javascript:index.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
const request = require('request');
require('date-utils');

/*************************************************************************/
/* Confidential information
/*************************************************************************/
const LINE_CHANNEL_ACCESS_TOKEN = 'XXXXX';    // LINE Botのアクセストークン
const LINE_CHANNEL_SECRET = 'XXXXX';          // LINE BotのChannel Secret
const DOCOMO_API_KEY = 'XXXXX';               // docomoAPI用：APIキー
const GURUNAVI_API_KEY = 'XXXXX';             // ぐるなびAPI用：APIキー
/*************************************************************************/

const PORT = process.env.PORT || 3000;
const LINE_MESSAGE_MAX_LENGTH = 2000;

const NEW_LINE = '\n';
const GURUNAVI_LUNCH_HOUR = 13;
const GURUNAVI_DRINKING_HOUR = 17;

const config = {
  channelAccessToken: LINE_CHANNEL_ACCESS_TOKEN,
  channelSecret: LINE_CHANNEL_SECRET
};

const app = express();

app.post('/webhook', line.middleware(config), (req, res) => {

  // 先行してLINE側にステータスコード200を返す
  res.sendStatus(200);

  console.log(req.body.events);
  Promise
    .all(req.body.events.map(handleEvent))
    .then((result) => {
      console.log('event processed.');
    });
});

const client = new line.Client(config);

function handleEvent(event) {

  console.log('handleEvent()');

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

  if ((event.message.type !== 'text') && (event.message.type !== 'location')) {
    return Promise.resolve(null);
  }

  let message = '';

  /***** type：text *****/
  const text = (event.message.type === 'text') ? event.message.text : '';
  if (text == '天気') {
    // 天気予報
    checkWeatherForecast(event.source.userId);

    message = 'ちょっと待ってね';

  } else if (text.endsWith('を調べて')) {
    // Wiki検索
    const length = 'を調べて'.length;
    const word = text.slice(0, -length);
    lookUpWords(event.source.userId, word);

    message = 'ちょっと待ってね';

  } else if (text.startsWith('言語解析 ')) {
    // 言語解析
    const length = '言語解析 '.length;
    const sentence = text.slice(length);
    analysisSentence(event.source.userId, sentence);

    message = 'ちょっと待ってね';

  } else if (text == 'ヘルプ') {
    // ヘルプ
    message = '有効キーワード' + NEW_LINE;
    message += '・天気' + NEW_LINE;
    message += '　→天気を調べるよ' + NEW_LINE;
    message += '・[単語]を調べて' + NEW_LINE;
    message += '　→Wikiで単語を調べるよ' + NEW_LINE;
    message += '・言語解析 [文章]' + NEW_LINE;
    message += '　→文章を解析するよ' + NEW_LINE;
    message += '　　スペースを忘れずにね' + NEW_LINE;
    message += '・[位置情報]' + NEW_LINE;
    message += '　→ぐるなびでお店を調べるよ' + NEW_LINE;
    message += '　　' + GURUNAVI_LUNCH_HOUR + ':00まではランチ' + NEW_LINE;
    message += '　　' + GURUNAVI_LUNCH_HOUR + ':00-' + GURUNAVI_DRINKING_HOUR + ':00はカフェ' + NEW_LINE;
    message += '　　' + GURUNAVI_DRINKING_HOUR + ':00以降は居酒屋' + NEW_LINE;
    message += 'その他は、おうむ返しするよ！';

  } else if (text != '') {
    // おうむ返し
    message = text;
  }

  /***** type：location *****/
  const latitude = (event.message.type === 'location') ? event.message.latitude : '';
  const longitude = (event.message.type === 'location') ? event.message.longitude : '';
  if ((latitude != '') && (longitude != '')) {
    // ぐるなび検索
    gurunaviSearch(event.source.userId, latitude, longitude);

    message = 'ちょっと待ってね';
  }

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

const checkWeatherForecast = async (userId) => {

  //情報提供元：ライブドアの天気API
  //http://weather.livedoor.com/weather_hacks/webservice

  console.log('checkWeatherForecast()');

  const city = '130010';  // 東京
  const url = 'http://weather.livedoor.com/forecast/webservice/json/v1?city=' + city;
  const res = await axios.get(url);
  const item = res.data;

  let message = '';

  // タイトル
  const title = item.title;
  message = '[' + title + ']' + NEW_LINE;

  // 今日、明日、明後日の天気
  const today = item.forecasts[0];
  const tomorrow = item.forecasts[1];
  const dayAfterTomorrow = item.forecasts[2];
  if (today) {
    message += today.dateLabel + '　：' + today.telop + NEW_LINE;
  }
  if (tomorrow) {
    message += tomorrow.dateLabel + '　：' + tomorrow.telop + NEW_LINE;
  }
  if (dayAfterTomorrow) {
    message += dayAfterTomorrow.dateLabel + '：' + dayAfterTomorrow.telop + NEW_LINE;
  }

  // 天気概況文
  //message += NEW_LINE;
  //message += item.description.text + NEW_LINE;

  // URL
  message += NEW_LINE;
  message += item.link;

  console.log('message=' + NEW_LINE);
  console.log(message);

  await client.pushMessage(userId, {
    type: 'text',
    text: message
  });
}

const lookUpWords = async (userId, word) => {

  //情報提供元：MediaWikiのAPI
  //https://www.mediawiki.org/wiki/API:Main_page/ja

  console.log('lookUpWords()');

  const options = {
    url: 'https://ja.wikipedia.org/w/api.php',
    qs: {
      format: 'json',
      action: 'query',
      redirects: 1,
      list: 'search',
      srsearch: word,
      srlimit: 3,           // 検索結果の最大取得件数
      prop: 'extracts',
      exchars: 200,         // 説明文の最大文字列長
      explaintext: 1,
    }
  };

  request(options, function(err, response, result) {

    let message = '';

    if(!err && response.statusCode == 200) {
      const json = JSON.parse(result);
      const search = json.query.search;
      const wikiURL = 'https://ja.wikipedia.org/wiki/';

      const mainTitle = '[検索結果]' + NEW_LINE;
      message = mainTitle;

      Object.keys(search).some(function(key) {
        if (key == -1) {
          message = 'ごめんなさい。' + NEW_LINE;
          message += '該当ワードはありません。';
          return true;
        }

        const item = search[key];
        if (item.title && item.snippet) {
          let itemMessage = '';

          if (message != mainTitle) {
            itemMessage = NEW_LINE;
            itemMessage += NEW_LINE;
          }

          const title =  item.title;
          let summary = item.snippet;
          summary = summary.replace(/<span class="searchmatch">/g, '');
          summary = summary.replace(/<\/span>/g , '');

          // ワード
          itemMessage += '◆' + title + 'とは' + NEW_LINE;

          // 説明文
          itemMessage += summary + NEW_LINE;

          // URL
          itemMessage += NEW_LINE;
          itemMessage += encodeURI(wikiURL + title);

          if ((message.length + itemMessage.length) > LINE_MESSAGE_MAX_LENGTH) {
            return true;
          }

          message += itemMessage;
        }
      });

      if (message == mainTitle) {
        message = 'ごめんなさい。' + NEW_LINE;
        message += '該当ワードはありません。';
      }

      console.log('message=' + NEW_LINE);
      console.log(message);

    } else {
      message = 'ごめんなさい。' + NEW_LINE;
      message += 'エラーが発生しました。';
      console.log('error!');
      console.log('err:' + err + ', response.statusCode:' + response.statusCode);
    }

    client.pushMessage(userId, {
      type: 'text',
      text: message
    });
  }).setMaxListeners(10);
}

const analysisSentence = async (userId, sentence) => {

  //情報提供元：docomoのAPI
  //https://dev.smt.docomo.ne.jp
  //https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=language_analysis&p_name=api_2#tag01

  console.log('analysisSentence()');

  const headers = {
    'Content-Type': 'application/json'
  }

  const strRequestId = Math.random().toString(36).slice(-8);
  const body = {
    request_id: strRequestId,
    sentence: sentence
  }

  const options = {
    url: 'https://api.apigw.smt.docomo.ne.jp/gooLanguageAnalysis/v1/entity',
    method: 'POST',
    qs: {
      APIKEY: DOCOMO_API_KEY
    },
    headers: headers,
    body: JSON.stringify(body)
  };

  request(options, function(err, response, result) {
    let message = '';

    if(!err && response.statusCode == 200) {
      const json = JSON.parse(result);

      if (json.request_id != strRequestId) {
        message = 'ごめんなさい。' + NEW_LINE;
        message += '解析できませんでした。';
      } else {
        const list = json.ne_list;

        const title = '[解析結果]' + NEW_LINE;
        message = title;

        Object.keys(list).some(function(key) {
          if (message != title) {
            message += NEW_LINE;
          }

          const item = list[key]
          const word = item[0];
          const type = item[1];
          message += type + '：' + word;
        });

        if (message == title) {
          message = 'ごめんなさい。' + NEW_LINE;
          message += '解析結果はありません。';
        }

        console.log('message=' + NEW_LINE);
        console.log(message);
      }

    } else {
      message = 'ごめんなさい。' + NEW_LINE;
      message += 'エラーが発生しました。';
      console.log('error!');
      console.log('err:' + err + ', response.statusCode:' + response.statusCode);
    }

    client.pushMessage(userId, {
      type: 'text',
      text: message
    });
  }).setMaxListeners(10);
}

const gurunaviSearch = async (userId, latitude, longitude) => {

  //情報提供元：ぐるなびのAPI
  //https://api.gnavi.co.jp/api/
  //https://api.gnavi.co.jp/api/manual/

  console.log('gurunaviSearch()');

  const url = 'https://api.gnavi.co.jp/RestSearchAPI/20150630/';
  const format = 'json';
  const range = 1;              // 緯度/経度からの検索範囲(半径)。1は、300m
  const hit_per_page = 3;       // 検索結果の最大取得件数

  let options;
  const nowHour = new Date().toFormat("HH24");
  if (nowHour < GURUNAVI_LUNCH_HOUR) {
    // ランチ検索
    options = {
      url: url,
      qs: {
        keyid: GURUNAVI_API_KEY,
        format: format,
        latitude: latitude,
        longitude: longitude,
        range: range,
        hit_per_page: hit_per_page,
        lunch: 1    // ランチ営業有無　0:絞込みなし(デフォルト)、1：絞込みあり
      }
    };
  } else {
    let category;
    if (nowHour < GURUNAVI_DRINKING_HOUR) {
      // 大業態「カフェ・スイーツ」で検索
      category = 'RSFST18000';
    } else {
      // 大業態「居酒屋」で検索
      category = 'RSFST09000';
    }

    options = {
      url: url,
      qs: {
        keyid: GURUNAVI_API_KEY,
        format: format,
        category_l: category,   // 大業態コード
        latitude: latitude,
        longitude: longitude,
        range: range,
        hit_per_page: hit_per_page
      }
    };
  }

  request(options, function(err, response, result) {
    let message = '';

    if(!err && response.statusCode == 200) {
      const json = JSON.parse(result);

      if (json.rest) {
        const list = json.rest;

        const title = '[検索結果]' + NEW_LINE;
        message = title;

        let number = 1;
        Object.keys(list).some(function(key) {
          if (message != title) {
            message += NEW_LINE;
          }

          const item = list[key];

          const name = item.name;
          if (name) {
            message += 'No.' + number + NEW_LINE;
            message += '◆店舗名：' + NEW_LINE;
            message += name + NEW_LINE;

            let opentime = item.opentime;
            if (opentime && (typeof opentime == 'string')) {
              message += '◆営業時間：' + NEW_LINE;
              opentime = opentime.replace(/<BR>/g, NEW_LINE);
              message += opentime + NEW_LINE;
            }

            let holiday = item.holiday;
            if (holiday && (typeof holiday == 'string')) {
              message += '◆休業日：' + NEW_LINE;
              holiday = holiday.replace(/<BR>/g, NEW_LINE);
              message += holiday + NEW_LINE;
            }

            const url = item.url_mobile;
            if (url) {
              message += url + NEW_LINE;
            }

            number++;
          }
        });

        if (message == title) {
          message = 'ごめんなさい。' + NEW_LINE;
          message += '検索結果はありません。';
        }

        console.log('message=' + NEW_LINE);
        console.log(message);

      } else {
        message = 'ごめんなさい。' + NEW_LINE;
        message += '検索結果はありません。';
      }

    } else {
      message = 'ごめんなさい。' + NEW_LINE;
      message += 'エラーが発生しました。';
      console.log('error!');
      console.log('err:' + err + ', response.statusCode:' + response.statusCode);
    }

    client.pushMessage(userId, {
      type: 'text',
      text: message
    });
  }).setMaxListeners(10);
}

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