位置情報を送ると最寄りのラーメン店を教えてくれるLINE botをつくってみた🍜（2020年版）

#概要
　
　この記事はLINE bot・javascrptの初心者が、「LINE bot 面白い！」という熱意で作ったものの制作記です。

　何をつくったか...。
「LINE botに位置情報を送ると、その近辺のラーメン屋さんを教えてくれるbot」を作りました🍜！

【デモ】
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">位置情報を入力すると最寄りのラーメンやさんを教えてくれるLINE botを作ってみた！<br>後でQiitaの記事を出します。 <a href="https://t.co/ZHCvzj6nqT">pic.twitter.com/ZHCvzj6nqT</a></p>&mdash; まえぷー@出窓菜園 BWG (@kmaepu) <a href="https://twitter.com/kmaepu/status/1243914106144321537?ref_src=twsrc%5Etfw">March 28, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

　本記事ではこのLINE botのサーバプログラムと、ラーメン店情報を検索するために**ぐるなび API**を解説しています。基礎的な機能のみ実装しているので、店舗の星の多さ、複数件表示などは未実装です。

　位置情報を送ると、その最寄りのラーメン屋さんを一軒返すところまで動かしました。
以下、2020年3月29日時点の情報です。

#開発環境
OS：Windows 10
Node.js：v10.15.3

【ライブラリバージョン】
@line/bot-sdk：6.8.4
express：4.17.1
axios：0.19.2

#構成
　構成は次のようになっています。ローカルPC上でNode.jsを動かしているので、グローバルネットワークとのつながりを[ngrok](https://qiita.com/mininobu/items/b45dbc70faedf30f484e)でトンネリングしています。

![構成図.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/38787a7e-8cf0-c4f6-0726-19f494bd1f5c.jpeg)

　位置情報をNode.jsサーバが受け取ると、[ぐるなび API](https://api.gnavi.co.jp/api/)を利用して最寄りののラーメン店情報を取得します。このとき、ユーザから送られた位置情報をぐるなび APIのリクエストURLに含めています。

#実装
##ぐるなび APIの調査
　[ぐるなび API](https://api.gnavi.co.jp/api/)はAPIの使用説明だけでなく、APIテストツールまで用意されていました。手始めに、**freeword**クエリを追加してリクエストしてみると、レスポンスが下の方に表示されます。
（アクセスキーはテスト用になっています）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/5f3ed565-2d25-ed7d-c528-fe3ef1732f6c.png)

　どんなクエリがあるかは、API仕様の[レストラン検索 API](https://api.gnavi.co.jp/api/manual/restsearch/)で公開されています。

##プログラムからアクセスしてみる

　LINE botとのやり取りと、APIアクセスは[こちら](https://qiita.com/kmaepu/items/d22722a303a4fd46fba5)のソースコードをもとに進めていきます。 
APIテストツールを使うと、リクエストURLが生成されるのでコピーしてaxisos.getに引数に与えます。
こんな感じです。

```javascript
axios.get('https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=b17d2f36873123b0d0f09e9139556c79&freeword=ラーメン');
```

　この状態でアクセスしたら、次のようなエラーが発生しました。

```
Request path contains unescaped characters
```

　コーディングミスというよりは、URLとして認識されていないようなエラーメッセージですね。試しにフリーワードをローマ字（ra-men）にしたらエラーが解消されて、レスポンスが返るようになりました！
↓レスポンスのログ

![ローマ字なら動いた.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/3fe8f289-1f9b-b523-1364-c86a416f68f9.jpeg)

　一先ず、これでレスポンスが返るので先に進みます。日本語対応は後述の「後回しにしていた日本語対応」で解説しています。

##最寄りの店に絞る
　フリーワード検索のみだと全国の店が検索されてしまうので、最寄りの店に絞ります。これはぐるなび APIのクエリに**経緯**を追加することで絞ることが可能です。

　最初は経緯を固定で動かします。経緯を調べるにはGoogleマップが便利です。今回はProtoOut Studioの本拠地であるgranicaにしてみました。

【リクエストURL」

```
https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=アクセスキー&latitude=35.704607&longitude=139.772416&range=2&freeword=ラーメン
```
　このURLで問題なくレスポンスが来ればOKです。

が、居酒屋が引っかかる！！！！！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/7b14b419-a706-db13-84d7-28854a8cfbd2.png)

##ラーメン店に絞る

　**ラーメン**に絞りたいので、レスポンスデータの中の**category**でフィルタリングしました。

```javascript
for(var num = 0; num <= response.data.rest.length; num++){
    if(response.data.rest[num].category === 'ラーメン'){
      console.log(response.data.rest[num]);
      ramen_url = response.data.rest[num].url_mobile;
      break;
    }
  }
```

　これで最寄りのラーメン店に絞ることができました！と、思いきや最寄りにラーメン店が見つからなかった場合、エラーが発生します。ちなみに、最寄りとは半径500m圏内です。**今回は正常系のみ作ることを目的とし、エラーハンドリングをスルーします...。**

## 後回しにしていた日本語対応

　フリーワードに日本語が入っていることが原因なので、URLにエンコードしてくれる**encodeURI関数**がありました。[こちら](https://teratail.com/questions/201480)の記事の通りになおすと、フリーワードに日本語が入っていても通るようになりました！
こんなコードです。

```javascript
url = 'https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=アクセスキー&latitude=35.704607&longitude=139.772416&range=2&freeword=ラーメン';
  const encodeUrl = encodeURI(url);
  const response = await axios.get(encodeUrl);
```

## LINE botへのレスポンスを位置情報にする

　このままテキストで返してもよいのですが、LINE botには**位置情報**を返す機能がありまので、位置情報で返してみました。ちなみに、テキストでURLを返すと次のようになります。
![Screenshot_20200328-230452_LINE.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/225daa76-ea72-f8bf-1e53-5f4b9cd15f83.jpeg)

これを位置情報にすると、

![Screenshot_20200328-231744_LINE.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183282/9d796566-7751-8f28-3da8-471d649859cd.jpeg)

こっちの方が見栄え良さそうですね　ｲｪｰｲ(/・ω・)/

 LINE botで位置情報を返すコードは次の通りです。経緯だけではなく住所も返す必要があります。
コード中のhitnumは、レスポンスデータの中で、category”ラーメン”にヒットした位置が格納されているとします。ぐるなび APIの良いところはこれらの情報が全てそろっていることです👍

```javascript
return client.replyMessage(event.replyToken, {
  type: 'location',
  title: response.data.rest[hitnum].name,
  address: response.data.rest[hitnum].address,
  latitude: response.data.rest[hitnum].latitude,
  longitude: response.data.rest[hitnum].longitude
});
```

##位置情報入力を追加

　最後に位置情報入力に対応させます。LINEから位置情報を受け取ってログに出し、URLに加えるコードは次の通りです。入力が位置情報のみとなるようにtypeで判定しています。

```javascript
　if (event.type !== 'message' || event.message.type !== 'location') {
    return Promise.resolve(null);
  }
  console.log(event.message.latitude + ' : ' + event.message.longitude);
　url = 'https://api.gnavi.co.jp/RestSearchAPI/v3/?keyid=アクセスキー&latitude=' + event.message.latitude + '&longitude=' + event.message.longitude + '&range=2&freeword=ラーメン';
```

#余談
　
　今回、正常系で動くところまで作れたのですが、ラーメン店が見つからないかった場合のエラーハンドリングを入れたかったなと思います。例えば、かわいい動物画像を出すとか。

　もう一つ思ったのは、jsonデータのコンソールログデバッグは時間がかかったなと。APIの解説がされているサイトをみて一気にコーディングできたらよいのですが...。まずは経験して慣れることが大切ですね。

　ちなみに、ProtoOut Studioの授業後から制作を開始し、Qiita公開まで6時間程度かかりました。
javascriptのデバッグ不慣れなのと、Qiita記事執筆をもう少し効率化できそうなので、もう少し頑張ってプロトアウトのサイクルを短くしてきたいなと思っています。

　ほんとに余談ですが、ProtoOut Studioの一環で進めているので、よくわからないエラーが発生して困っていると講師の方々に相談できます。校長である菅原のびすけさんに直接コーディング指南していただいてものすごく緊張していました(;'∀')

 　今回はぐるなびAPIを利用しましたが、他にも猫の種類検索API[この猫なに猫？](https://metadata.co.jp/apis/what-cat.html)や[Harry Potter API](https://www.potterapi.com/)など気になるAPIが沢山あります(^^♪

　今後はこのLINE botをベースに、connpass APIやGoogle MAP APIなどと連携して、勉強会やイベントのお知らせと夕食を提案してくれるようなサービスにしていけたら面白そうかなと思っています。

#参考
- [LINE Deveropers](https://developers.line.biz/ja/reference/messaging-api/#wh-location)
- [APIbank](https://www.apibank.jp/ApiBank/main;jsessionid=7DAB604DF8524F10B758984E7F830B82)
- [ぐるなび API](https://api.gnavi.co.jp/api/)
- [JavaScriptでぐるなびAPIを使う方法](https://qiita.com/hirosehy/items/cb39c6449ca8613329e0)
- [LINE MessagingAPI でできることまとめ【送信編】](https://qiita.com/kakakaori830/items/52e52d969800de61ce28)

#ソースコード

```javascript

'use strict';

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

const config = {
    channelSecret: 'LINE botのチャンネルシークレット',
    channelAccessToken: 'LINE botのアクセストークン'
};

const app = express();

app.get('/', (req, res) => res.send('Hello LINE BOT!(GET)')); //ブラウザ確認用(無くても問題ない)
app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);

    //ここのif文はdeveloper consoleの"接続確認"用なので後で削除して問題ないです。
    if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){
        res.send('Hello LINE BOT!(POST)');
        console.log('疎通確認用');
        return; 
    }

    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);


async function handleEvent(event) {
  var url
  var ramen_url;
  var hitnum;

  // 位置情報のみに入力制限
  if (event.type !== 'message' || event.message.type !== 'location') {
    return Promise.resolve(null);
  }

  // 取得した位置情報をログに表示
  console.log(event.message.latitude + ' : ' + event.message.longitude);

  // ぐるなびAPIを使うためのURLに経緯を加える
  url = 'https://api.gnavi.co.j
p/RestSearchAPI/v3/?keyid=af2b4862cbe801c5e2e1b87ef52881f6&latitude=' + event.message.latitude + '&longitude=' + event.message.longitude + '&range=2&freeword=ラーメン';
  const encodeUrl = encodeURI(url);

  // ぐるなびAPIに問い合わせ
  const response = await axios.get(encodeUrl);

  //レスポンスの中からcategory"ラーメン"を探索
  for(var num = 0; num <= response.data.rest.length; num++){
    if(response.data.rest[num].category === 'ラーメン'){
//      console.log(response.data.rest[num]);
      hitnum = num;
      ramen_url = response.data.rest[num].url_mobile;
      break;
    }
  }
 
// ヒットしたラーメン店の住所をLINE botに返す
return client.replyMessage(event.replyToken, {
  type: 'location',
  title: response.data.rest[hitnum].name,
  address: response.data.rest[hitnum].address,
  latitude: response.data.rest[hitnum].latitude,
  longitude: response.data.rest[hitnum].longitude
});
}
app.listen(PORT);
console.log(`Server running at ${PORT}`);
```