8
20

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 5 years have passed since last update.

無料APIを使ったLINE Bot

Last updated at Posted at 2018-06-23

#はじめに

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

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

#目次

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

#使ったAPIの紹介と感想

  • livedoor提供のお天気情報API
    • 概要:お天気情報を取得するAPI
    • API仕様:http://weather.livedoor.com/weather_hacks/webservice
    • サンプルコード:以下 checkWeatherForecast を参照
    • 感想:気軽に確認できるので便利。

         降水確率などの細かい情報は取得できないのでURL表示して飛べるようにするのが良い。


* 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 を参照

* docomo提供のAI系API - 概要:言語解析、音声認識、画像認識、文字認識などができるAPI - API仕様:https://dev.smt.docomo.ne.jp/?p=docs.common.index
    ※今回使ったAPIは、[「固有表現抽出」](https://dev.smt.docomo.ne.jp/?p=docs.api.page&api_name=language_analysis&p_name=api_2#tag01)のみ
    ※API利用には、docomo Developer supportのアカウント登録が必要 - サンプルコード:以下 analysisSentence を参照 - 感想:今回対応した使い方は、日々の情報取得を便利にするといったものではないけれど
   「どの程度解析できるようになっているのかなー」という個人的興味を満たしてくれるので
    他のAPIも使ってみたいところ。

* ぐるなび提供の飲食店情報API - 概要:ぐるなびの飲食店情報を取得するAPI - API仕様:https://api.gnavi.co.jp/api/manual/
    ※今回使ったAPIは、[「レストラン検索API」](https://api.gnavi.co.jp/api/manual/restsearch/)
    ※API利用には、ぐるなびWEBサービスでのアカウント登録が必要 - サンプルコード:以下 gurunaviSearch を参照 - 感想:「よく検索する条件の指定」と「見たい検索結果情報のみ表示」ができるAPIになっているので
    個人的にはなかなか使い勝手がよい。

#画面イメージ

livedoor提供のお天気情報API
スクリーンショット_天気.png

Wikiソフトウェア「MediaWiki」
スクリーンショット_Wiki検索.png

docomo提供のAI系API
スクリーンショット_言語解析.png

ぐるなび提供の飲食店情報API
スクリーンショット_ぐるなび検索.png

ヘルプ
スクリーンショット_ヘルプ.png

その他:おうむ返し
スクリーンショット_おうむ返し.png

#サンプルコード

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

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}`);
8
20
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
8
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?